Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Context: Supabase Operator

Glossary of domain terms for this repo. Behavior language only, no implementation detail.

## Terms

### SupabaseProject
The single CRD of this operator. Namespace scoped. Describes one Supabase instance: which external database and object storage to use, and per component settings. The operator deploys and manages all Supabase components (Kong, Auth, PostgREST, Realtime, Storage API, Meta, Studio) from it.

### External dependency
A service the operator needs but never creates or manages: PostgreSQL and S3 compatible object storage. The user provides both and hands the operator connection credentials through Secrets. The operator only validates and consumes them.

### Secret contract
The exact key names a credentials Secret must contain so the operator can consume it.
- Database Secret keys: `host`, `port`, `database`, `username`, `password`
- Storage Secret keys: `endpoint`, `region`, `bucket`, `accessKeyId`, `secretAccessKey`

### Quickstart
The documented golden path for a first time user: install the operator, install external dependencies with well known Helm charts (CloudNativePG for PostgreSQL, MinIO for object storage), create the credential Secrets, apply one SupabaseProject, reach a working Studio via port-forward. Target is a working Studio on a fresh cluster in minutes.

### Database initialization
The one time setup the operator runs against the external PostgreSQL after a project is created: creating the `_supabase` database, Supabase schemas, service roles, and extensions. Requires a superuser grade connection and the `supabase/postgres` image, because Supabase specific extensions must be present.

### dev scaffolding
The manifests under `dev/` used by maintainers for local development. Not user facing, not part of the quickstart.
31 changes: 31 additions & 0 deletions docs/adr/0001-quickstart-via-charts-not-managed-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ADR 0001: Quickstart uses well known Helm charts, not an operator managed mode

Date: 2026-07-25

Status: accepted

## Context

Issue #18 planned a "managed mode": `spec.database.managed: true` would make the operator provision an in-cluster PostgreSQL StatefulSet, and a similar flag would provision MinIO. The goal was a first working project in under 5 minutes on a bare cluster.

Building that inside the operator means the operator takes on database lifecycle work: storage, upgrades, credentials, failure recovery. That is a large and permanent maintenance load, and mature tools already do it better.

A second constraint: the operator only supports the `supabase/postgres` image, because database initialization needs Supabase specific extensions (pgjwt, pg_net and others) and superuser access. Any install path must produce a PostgreSQL running that image.

## Decision

Do not build managed mode. Keep PostgreSQL and S3 strictly user provided.

Instead, ship a documented quickstart that installs the dependencies with well known Helm charts:

- PostgreSQL via the CloudNativePG operator, with the cluster image set to `supabase/postgres`. Known required settings: `postgresUID: 101`, `postgresGID: 102` (the image does not use the CNPG default uid 26), and `enableSuperuserAccess: true` (database initialization needs superuser).
- Object storage via the official MinIO chart (`charts.min.io`), standalone mode, bucket declared through chart values.

The quickstart manifests live in the repo so users can apply them directly and docs cannot drift from them.

## Consequences

- The operator stays thin: it never owns database or storage lifecycle.
- The 5 minute goal is met by documentation quality, not new operator code paths.
- The CNPG plus `supabase/postgres` combination is community proven but not officially supported by either project; the quickstart must be verified end to end whenever images are bumped.
- Issue #18 Phase A tasks about managed mode are superseded by this decision.
239 changes: 239 additions & 0 deletions docs/quickstart/VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Quickstart validation

This file records a successful run on a fresh kind cluster.

## Tested versions

```text
kind v0.32.0
Kubernetes v1.36.1
CloudNativePG chart 0.29.0
CloudNativePG 1.30.0
MinIO chart 5.4.0
MinIO RELEASE.2024-12-18T13-15-44Z
cert-manager v1.21.0
Supabase PostgreSQL 15.14.1.021
Supabase Operator image ghcr.io/strrl/supabase-operator:358f551-dirty
Supabase Operator chart 0.1.0
```

## Commands

Create the cluster and add the chart sources.

```sh
kind create cluster --name quickstart-claude --wait 120s

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo add minio https://charts.min.io/
helm repo add jetstack https://charts.jetstack.io
helm repo update cnpg minio jetstack
```

Install CloudNativePG and create PostgreSQL.

```sh
helm upgrade --install cnpg cnpg/cloudnative-pg \
--kube-context kind-quickstart-claude \
--namespace cnpg-system \
--create-namespace \
--version 0.29.0 \
--wait \
--timeout 5m

kubectl --context kind-quickstart-claude \
apply -f docs/quickstart/postgres-cluster.yaml
kubectl --context kind-quickstart-claude \
wait --namespace supabase-quickstart \
--for=condition=Ready \
cluster/supabase-postgres \
--timeout 10m
kubectl --context kind-quickstart-claude \
get cluster supabase-postgres \
--namespace supabase-quickstart \
-o custom-columns='NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,IMAGE:.spec.imageName,UID:.spec.postgresUID,GID:.spec.postgresGID'
```

Confirm that PostgreSQL accepts a TLS connection.

```sh
kubectl --context kind-quickstart-claude \
run postgres-tls-check \
--namespace supabase-quickstart \
--image=postgres:15-alpine \
--restart=Never \
--command \
-- sh -c 'PGPASSWORD=quickstart-postgres-password psql "host=supabase-postgres-rw port=5432 dbname=postgres user=postgres sslmode=require" -Atc "select ssl from pg_stat_ssl where pid = pg_backend_pid()"'
kubectl --context kind-quickstart-claude \
wait --namespace supabase-quickstart \
--for=jsonpath='{.status.phase}'=Succeeded \
pod/postgres-tls-check \
--timeout 2m
kubectl --context kind-quickstart-claude \
logs postgres-tls-check \
--namespace supabase-quickstart
```

Install MinIO and confirm the declared bucket.

```sh
helm upgrade --install minio minio/minio \
--kube-context kind-quickstart-claude \
--namespace supabase-quickstart \
--create-namespace \
--version 5.4.0 \
--values docs/quickstart/minio/values.yaml \
--wait \
--timeout 5m
kubectl --context kind-quickstart-claude \
rollout status deployment/minio \
--namespace supabase-quickstart \
--timeout 5m
kubectl --context kind-quickstart-claude \
run minio-check \
--namespace supabase-quickstart \
--image=quay.io/minio/mc:RELEASE.2024-11-21T17-21-54Z \
--restart=Never \
--command \
-- sh -c 'mc alias set quickstart http://minio:9000 storage-admin quickstart-minio-password >/dev/null && mc stat quickstart/supabase-storage'
kubectl --context kind-quickstart-claude \
wait --namespace supabase-quickstart \
--for=jsonpath='{.status.phase}'=Succeeded \
pod/minio-check \
--timeout 2m
kubectl --context kind-quickstart-claude \
logs minio-check \
--namespace supabase-quickstart
```

Install cert-manager, apply the Secrets, build the operator, and install its
chart.

```sh
helm upgrade --install cert-manager jetstack/cert-manager \
--kube-context kind-quickstart-claude \
--namespace cert-manager \
--create-namespace \
--version v1.21.0 \
--set crds.enabled=true \
--wait \
--timeout 5m

kubectl --context kind-quickstart-claude \
apply -f docs/quickstart/database-secret.yaml
kubectl --context kind-quickstart-claude \
apply -f docs/quickstart/storage-secret.yaml
kubectl --context kind-quickstart-claude \
apply -f docs/quickstart/studio-basic-auth-secret.yaml

OPERATOR_IMAGE_TAG="$(bash hack/commit-hash.sh)"
make image
kind load docker-image \
"ghcr.io/strrl/supabase-operator:${OPERATOR_IMAGE_TAG}" \
--name quickstart-claude

helm upgrade --install supabase-operator ./helm/supabase-operator \
--kube-context kind-quickstart-claude \
--namespace supabase-operator-system \
--create-namespace \
--set image.repository=ghcr.io/strrl/supabase-operator \
--set "image.tag=${OPERATOR_IMAGE_TAG}" \
--set image.pullPolicy=IfNotPresent \
--wait \
--timeout 5m
```

Apply the project and check every component.

```sh
kubectl --context kind-quickstart-claude \
apply -f docs/quickstart/supabase-project.yaml
kubectl --context kind-quickstart-claude \
wait --namespace supabase-quickstart \
--for=jsonpath='{.status.phase}'=Running \
supabaseproject/quickstart \
Comment on lines +153 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for component availability before testing Studio

On a fresh cluster or during slow image pulls, this wait can finish before any component pod is ready: reconcileAllComponents only creates/updates the Deployments, and internal/controller/supabaseproject_controller.go:158-168 immediately sets status.phase to Running without checking Deployment availability. The following kubectl get also succeeds when READY is zero, so the subsequent Kong port-forward can race with startup and make the documented validation intermittently fail; wait for each component Deployment to become Available (or use kubectl rollout status) before testing Studio.

Useful? React with 👍 / 👎.

--timeout 10m
kubectl --context kind-quickstart-claude \
get supabaseproject quickstart \
--namespace supabase-quickstart \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase'
kubectl --context kind-quickstart-claude \
get deployment \
--namespace supabase-quickstart \
-o custom-columns='NAME:.metadata.name,READY:.status.readyReplicas,DESIRED:.spec.replicas'
```

In one terminal, forward Kong.

```sh
kubectl --context kind-quickstart-claude \
port-forward service/quickstart-kong 18000:8000 \
--namespace supabase-quickstart
```

In another terminal, check the unauthenticated and authenticated responses.

```sh
curl \
--output /dev/null \
--silent \
--write-out 'HTTP %{http_code}\n' \
http://127.0.0.1:18000/
curl \
--user supabase:quickstart-studio-password \
--location \
--output /dev/null \
--silent \
--show-error \
--write-out 'HTTP %{http_code}\n' \
http://127.0.0.1:18000/
```

## Selected outputs

```text
CloudNativePG:
NAME READY IMAGE UID GID
supabase-postgres True supabase/postgres:15.14.1.021 101 102

Database TLS check:
t

MinIO:
Name : supabase-storage
Type : folder
Location : us-east-1
Objects count: 0

SupabaseProject:
NAME PHASE
quickstart Running

Deployments:
minio 1 1
quickstart-auth 1 1
quickstart-kong 1 1
quickstart-meta 1 1
quickstart-postgrest 1 1
quickstart-realtime 1 1
quickstart-storage 1 1
quickstart-studio 1 1

Studio through Kong:
HTTP 401
HTTP 200
```

## Static checks

```sh
kubectl --context kind-quickstart-claude \
apply --dry-run=client -f docs/quickstart/
helm template minio minio/minio \
--kube-context kind-quickstart-claude \
--namespace supabase-quickstart \
--version 5.4.0 \
--values docs/quickstart/minio/values.yaml
helm lint ./helm/supabase-operator
git diff --check
```
12 changes: 12 additions & 0 deletions docs/quickstart/database-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: supabase-db-credentials
namespace: supabase-quickstart
type: Opaque
stringData:
host: supabase-postgres-rw.supabase-quickstart.svc.cluster.local
port: "5432"
database: postgres
username: postgres
password: quickstart-postgres-password
23 changes: 23 additions & 0 deletions docs/quickstart/minio/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
mode: standalone

rootUser: storage-admin
rootPassword: quickstart-minio-password

replicas: 1

persistence:
enabled: true
size: 1Gi

resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 512Mi
cpu: 500m

buckets:
- name: supabase-storage
policy: none
purge: false
30 changes: 30 additions & 0 deletions docs/quickstart/postgres-cluster.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: v1
kind: Namespace
metadata:
name: supabase-quickstart
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-superuser
namespace: supabase-quickstart
type: kubernetes.io/basic-auth
stringData:
username: postgres
password: quickstart-postgres-password
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: supabase-postgres
namespace: supabase-quickstart
spec:
instances: 1
imageName: supabase/postgres:15.14.1.021
postgresUID: 101
postgresGID: 102
enableSuperuserAccess: true
superuserSecret:
name: postgres-superuser
storage:
size: 1Gi
12 changes: 12 additions & 0 deletions docs/quickstart/storage-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: supabase-storage-credentials
namespace: supabase-quickstart
type: Opaque
stringData:
endpoint: http://minio.supabase-quickstart.svc.cluster.local:9000
region: us-east-1
bucket: supabase-storage
accessKeyId: storage-admin
secretAccessKey: quickstart-minio-password
9 changes: 9 additions & 0 deletions docs/quickstart/studio-basic-auth-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: supabase-studio-basic-auth
namespace: supabase-quickstart
type: Opaque
stringData:
username: supabase
password: quickstart-studio-password
Loading
Loading