CloudNative Supabase is a Kubernetes operator that manages a focused,
rebuildable Supabase platform on CloudNativePG (CNPG). A SupabaseProject
creates PostgreSQL, GoTrue Auth, PostgREST, Studio, postgres-meta, and an
Envoy API gateway. PowerSync is optional.
This branch is an intentional breaking release. It has one authentication
architecture: opaque sb_publishable_* and sb_secret_* API credentials,
ES256 user sessions, and public-key verification. Kong, HS256 project JWTs,
and the legacy jwt/secrets CRD fields are removed.
Envoy listens on the <project>-api-gw Service at port 8000. It accepts only
the two configured opaque API keys and translates them to pre-signed anon
and service_role ES256 role tokens before routing to Auth, REST, Meta, or
Studio. GoTrue owns user-session signing; PostgREST and PowerSync verify with
public JWKS. No verifier receives a symmetric signing secret.
The Envoy admin API (including credential-bearing config_dump) is bound to
the pod loopback interface. Liveness and readiness use the harmless public
/_internal/health route instead, so other pods can reach a useful probe but
cannot inspect rendered gateway configuration.
The managed profile deliberately excludes Storage, Realtime, Functions, Analytics, and other upstream services that this operator does not deploy.
The Envoy assets are adapted from the official self-hosted Supabase assets at
upstream commit
95ca3024398080ff18c9abcd1c6c8beae73fd9e1.
Pinned images are Envoy envoyproxy/envoy:v1.39.0, GoTrue
supabase/gotrue:v2.189.0, and PostgREST postgrest/postgrest:v14.12.
Every project must reference one externally managed Secret in its namespace:
spec:
projectCredentialsSecret: my-project-credentialsThe Secret contains exactly these string fields:
| Key | Meaning |
|---|---|
signingKeys |
JSON array containing exactly one signing-capable P-256 ES256 private JWK with a non-empty kid |
publishableKey |
Canonical sb_publishable_<22 Base64URL random>_<8 Base64URL checksum> client credential (46 characters) |
secretKey |
Canonical sb_secret_<22 Base64URL random>_<8 Base64URL checksum> backend credential (41 characters), distinct from the publishable key |
anonRoleJwt |
ES256 JWT for role anon, audience authenticated |
serviceRoleJwt |
ES256 JWT for role service_role, audience authenticated |
The operator validates all five fields and their signatures before touching a
dependent workload. It derives a public-only JWKS ConfigMap for PostgREST and
generates an independent, create-once GoTrue fallback secret. Validation
errors set SecretsReady=False without putting credential contents in status.
The fallback value is used only by GoTrue; it is not part of the signing-key
array or public JWKS.
The opaque values are the self-hosted format used by Supabase. After the
role-specific prefix, each key has exactly 22 unpadded Base64URL characters,
one underscore separator at that fixed boundary, and exactly 8 unpadded
Base64URL checksum characters. The random segment may itself contain
underscores. The checksum is the first 8 Base64URL characters of SHA-256 over
supabase-self-hosted|<complete prefix plus random segment>. This literal
context and algorithm match the pinned Supabase self-hosted v0.7.0 scripts:
add-new-auth-keys.sh
and
rotate-new-api-keys.sh.
The format is also documented in Supabase's
self-hosted-auth-keys.mdx.
The evidence and enforcement boundary are collected in
docs/research/supabase-self-hosted-opaque-api-keys.md.
The operator rejects noncanonical opaque values before reconciling dependent workloads; there is no legacy or dual-format compatibility mode. Before deploying an operator image with this strict validator, rotate both opaque keys for every existing project and atomically update the matching client and backend consumers. This rotation is independent of the ES256 signing key and does not invalidate user sessions.
The operator does not integrate with Infisical. A typical deployment stores
the five values at one Infisical project/environment path and uses the
Infisical Kubernetes operator to synchronize them into the orphaned Secret
above. Keep signingKeys as its JSON string; do not wrap the five values in a
second JSON document or store the derived JWKS in Infisical.
Generate a complete bundle with the repository wizard instead of assembling the fields independently:
go run ./cmd/project-credentials-wizardRun it yourself in a trusted interactive terminal; it refuses redirected input or output. The wizard generates the values in process memory, validates the complete bundle with the operator's production validator, and copies each field through OSC52 without rendering plaintext or writing credentials to disk. A rerun creates a new atomic bundle; never combine fields from separate runs.
For example, an Infisical InfisicalStaticSecret can target the same
namespace with creationPolicy: Orphan (the auth objects and credentials are
created separately):
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: example-project-credentials-sync
namespace: supabase
spec:
infisicalAuthRef:
name: infisical-auth
namespace: supabase
syncOptions:
refreshInterval: 60s
sources:
- projectId: <infisical-project-id>
environmentSlug: dev
secretPath: /supabase/example
targets:
- name: example-project-credentials
namespace: supabase
kind: Secret
creationPolicy: OrphanThat path contains exactly signingKeys, publishableKey, secretKey,
anonRoleJwt, and serviceRoleJwt; the resulting Secret remains independent
of the SupabaseProject owner lifecycle.
apiVersion: supabase.guion.dev/v1alpha1
kind: SupabaseProject
metadata:
name: example
namespace: supabase
spec:
projectCredentialsSecret: example-project-credentials
database:
instances: 1
storage:
size: 20Gi
storageClass: local-path
auth:
siteURL: https://app.example.com
externalURL: https://auth.example.com
accessTokenExpirationSeconds: 3600
gateway:
replicas: 1All core services are always deployed. rest, studio, meta, and
gateway fields customize images, replicas, and resources; omission uses the
operator defaults. auth.goTrueEnv remains available for provider settings,
but JWT keys, fallback secret, key ID, issuer, audience, lifetime, valid
methods, and role settings are operator-owned and cannot be overridden.
SupabaseProject is the single supported customization interface for the
generated CNPG PostgreSQL projection. Its current database.parameters,
additional roles, platform HBA rules, and platform preload libraries are
assigned exactly on every reconcile, so removing a declaration or correcting a
direct edit converges on the project declaration. Direct edits to those
managed CNPG fields are unsupported and are reconciled away; fields outside
this explicit projection retain their existing CNPG/operator behavior.
Recovery and backup are independent. A new cluster can recover from one object store and immediately archive WAL and schedule backups to another:
spec:
database:
storage: {size: 100Gi}
recovery:
enabled: true
serverName: source-cluster
destinationPath: s3://recovery-bucket/source
s3CredentialsSecret: recovery-s3
backup:
enabled: true
destinationPath: s3://backup-bucket/example
s3CredentialsSecret: backup-s3
schedule: "0 0 2 * * *"
retentionPolicy: 30dRecovery bootstrap identity is creation-time state and cannot be changed after
the CNPG Cluster exists. Its destination path, endpoint, server name, exact
bootstrap source, and recovery target remain immutable; the recovery
s3CredentialsSecret is operational access and can rotate after bootstrap.
Supported mutable settings (instances, image, resources, superuser access,
PostgreSQL parameters, managed roles, backup plugin, and storage expansion)
continue to converge. Storage shrink is rejected.
Backup and recovery always use distinct ObjectStore names and must use distinct
configured destinationPath values. They may point at the same credentials
Secret; use least-privilege IAM so recovery can read its source while backup can
write its destination.
The CNPG Cluster, recovery/backup ObjectStores, and ScheduledBackup are
durable resources and are not owned by SupabaseProject. Deleting a project
therefore garbage-collects runtime services and generated configuration while
retaining the database and backup infrastructure. Recreating the same project
name adopts those retained resources without replacing the database. Durable
resources are mapped back to projects by namespace, deterministic name, and
an exact instance label; missing or foreign labels are never adopted or
deleted.
Explicit deletion of retained resources belongs to the migration runbook. For preserved-project cutovers, keep the old database for the agreed minimum 72-hour observation period before deleting it.
The status conditions include SecretsReady, DatabaseReady,
BackupReady, RecoveryReady, AuthReady, RestReady, StudioReady,
MetaReady, GatewayReady, and optional PowerSync/CDC conditions. The API
endpoint is <project>-api-gw:8000; the database endpoint is the CNPG
<project>-pg-rw:5432 Service.
Each core service condition and its availableReplicas value reflects the
observed Deployment, not merely the existence of its Kubernetes objects. A
core service is ready only after the Deployment controller has observed its
current generation and all desired replicas are updated, ready, and available
with no unavailable replicas. During creation or a partial rollout, pending
components remain Ready=False, the project phase is Provisioning, and the
aggregate Ready condition is false. The project reports Running with
aggregate Ready=True only after the database, every core service, and any
enabled PowerSync workloads are ready; observedGeneration advances to the
current project generation at that point.
go test ./...
go build ./...
make generate manifests
make test test-tanka test-delivery
make lint # when the pinned linter is availablemake test uses test-owned envtest binaries. No command above deploys an
operator or mutates a live cluster.
There is no compatibility or hybrid mode. Replace manifests using
spec.jwt, spec.secrets, or spec.kong with projectCredentialsSecret
and gateway, provision the five external credential fields, and plan a
coordinated client cutover to opaque keys and ES256 sessions. Existing
databases can be retained and readopted; callers must not expect old HS256
tokens or Kong routes to continue working. Existing projects with
noncanonical opaque keys must complete the two-key rotation and matching
consumer cutover before the strict operator image is rolled out.