Overview
Supabase is an open-source backend-as-a-service built on PostgreSQL: authentication, an auto-generated REST and GraphQL API, realtime database subscriptions, file storage and a web dashboard, all driven by one Postgres database. This template deploys the full self-hosted stack — a Supabase-patched Postgres 15 with pgvector, pg_graphql, pg_net and pgjwt pre-installed, plus GoTrue, PostgREST, Realtime, Storage and Studio behind a Kong API gateway — into your own GVC, with no dependency on Supabase Cloud. Kong is the single entry point and the authorization layer: API keys map to consumer groups, so the publicanonKey reaches the API routes while the admin routes and the dashboard login are restricted.
What Gets Created
Prerequisites
Three secrets must exist before you install — none of their values ever passes through Helm values, so none lands in the Helm release. Secrets are org-level, so no--gvc flag is involved.
JWT signing secret and API keys
secret, anonKey, serviceRoleKey and secretKeyBase. anonKey and serviceRoleKey must be HMAC-SHA256 JWTs signed by secret — you cannot invent them independently. This snippet mints all four consistently; run it as-is:jwt.secretName to this name. Your client apps need anonKey; read it back later with cpln secret reveal JWT_SECRET_NAME -o yaml. serviceRoleKey bypasses row-level security — keep it server-side.Postgres credentials
password and database. The login role name is fixed at postgres by the Supabase image, so it is not part of the secret:postgres.credentialsSecretName to this name.Studio dashboard password
studio.enabled: true (the default). An opaque secret with --encoding plain holding only the dashboard password on a single line. Kong enforces it as HTTP basic auth in front of the dashboard, so it is the only thing between the internet and full database control once Kong is public:studio.passwordSecretName to this name.File storage bucket (default install)
storage.backend: s3 targets AWS S3 and needs a bucket, a Control Plane Cloud Account and a bucket-scoped IAM policy — set storage.s3.bucket, storage.s3.region, storage.s3.cloudAccountName and storage.s3.policyName. The IAM policy must grant s3:GetObject, s3:PutObject and s3:DeleteObject on the bucket. For an S3-compatible store (MinIO, Cloudflare R2, Wasabi, Backblaze B2) or GCS, create a dictionary secret holding the access key pair instead — see Storage. Install with --set storage.enabled=false if your app has no file uploads.SMTP password (optional)
auth.smtp.enabled: true. An opaque secret with --encoding plain holding the SMTP password:auth.smtp.passwordSecretName to this name.OAuth client secrets (optional)
auth.providers.github.clientSecretName (or auth.providers.google.clientSecretName) to this name — see Auth.Installation
A default install needs the three secret names plus the S3 storage bucket details:storage.s3.* lines with --set storage.enabled=false to install without file storage. Or install using your preferred method:
UI
CLI
Terraform
Pulumi
Configuration
Postgres
postgres.image— Must stay asupabase/postgrestag. GoTrue, PostgREST, Realtime and Storage depend on extensions (pgvector, pg_graphql, pg_net, pgjwt) that the stockpostgresimage does not ship.postgres.credentialsSecretName— The dictionary secret from Prerequisites. The login role is alwayspostgres; the same password is applied to the internal service roles on first boot.postgres.internalAccess.type— Who can reach Postgres directly on port5432. All Supabase services connect inside the GVC; useworkload-listwithworkloadsto add your own application workloads. PgBouncer inherits the same setting.
Kong API Gateway
kong.publicAccess.enabled— Publish Kong on the internet. This also publishes the Studio dashboard at/behind its basic-auth login whenstudio.enabled: true.kong.publicAccess.siteUrl— Required when public access is on. GoTrue puts it in OAuth redirect callbacks and magic-link emails, so it must be the HTTPS URL your clients actually reach Kong at: a custom domain attached toRELEASE_NAME-kong, or the workload’s canonical*.cpln.appendpoint. With public access off, the template falls back to the internal Kong hostname and email or OAuth links only work inside the GVC.
apikey header they carry:
PostgREST
minReplicas and maxReplicas. Reachable only through Kong.
Auth
GoTrue provides email/password, magic links, OAuth and JWT sessions.auth.smtp.*— Required for magic links, signup confirmation and password reset. With SMTP disabled, signups are auto-confirmed and the email flows are unavailable. Any SMTP provider works; the password is the opaque secret from Prerequisites.auth.providers.github/auth.providers.google— The two OAuth providers this template wires up. Uncomment a block, set the provider’s client ID (not sensitive) and name the prerequisite opaque secret holding its client secret. In the provider’s developer console set the JavaScript origin tokong.publicAccess.siteUrland the redirect URI toSITE_URL/auth/v1/callback. OAuth needskong.publicAccess.enabled: true— providers will not redirect to internal hostnames.
Realtime
Storage
storage.s3.endpoint and storage.s3.region for the provider — a MinIO workload in the same GVC is http://WORKLOAD_NAME.GVC_NAME.cpln.local:9000 with region us-east-1 (its internalAccess must admit RELEASE_NAME-storage); Cloudflare R2 is https://ACCOUNT_ID.r2.cloudflarestorage.com with region auto; Wasabi and Backblaze B2 use their regional S3 endpoint and the bucket’s region. For GCS, generate the HMAC key under Cloud Storage → Settings → Interoperability for a service account with Storage Object Admin on the bucket.
The storage bucket and the backup bucket must be separate — each with its own bucket, Cloud Account and IAM policy.
Studio
/ route using studio.username and the password secret — Studio itself has no login. Reach it through Kong (see Connecting). studio.allowedCidrs and a wider studio.internalAccess.type open the Studio workload directly, which skips Kong and therefore skips the password: anyone who reaches it gets an unauthenticated dashboard holding the serviceRoleKey. Leave both at their defaults unless you have a reason, and never set 0.0.0.0/0.
JWT
secret is the HMAC-SHA256 signing key every service verifies tokens against; anonKey (role anon) is the public client key; serviceRoleKey (role service_role) bypasses row-level security and is the full-admin credential for your data — server-side only, never in client code; secretKeyBase is Realtime’s cookie-signing key. The keys must be signed by secret or every service returns bad_jwt — mint them together with the snippet in Prerequisites.
PgBouncer
transaction mode suits most web and API workloads but breaks session-level features (SET variables, temporary tables, advisory locks); use session if you need those. It accepts connections per postgres.internalAccess.
Backup
Connecting
All API traffic goes through Kong. Read the public address fromstatus.canonicalEndpoint in cpln workload get RELEASE_NAME-kong --gvc GVC_NAME -o yaml.
/rest/v1/ (PostgREST), /auth/v1/ (GoTrue), /storage/v1/ (Storage), /realtime/v1/ (Realtime), /pg/ (pg_meta, serviceRoleKey only). The Supabase client libraries set the apikey header for you:
First Run
Wait for the gateway to report ready
Open the dashboard
http://localhost:8000. Kong prompts for the dashboard credentials — studio.username and the password in your Studio secret — and then serves Studio with its Default Project. Reaching the dashboard through the tunnel has not been exercised against a live install of this template; for daily use publish Kong.Publish the gateway
RELEASE_NAME-kong or use its canonical endpoint, then upgrade the release with kong.publicAccess.enabled=true and kong.publicAccess.siteUrl set to that HTTPS address. The new URL lands in the Auth and Studio workload specs, so they roll on their own. Configure SMTP or OAuth only after siteUrl is right — both embed it in the links they send.Hand the anon key to your apps
cpln secret reveal JWT_SECRET_NAME -o yaml — give clients anonKey only. Protect your tables with row-level security before exposing the API publicly; anonKey reaches /rest/v1/ directly.Operations
Backing Up
Backups are off by default. Pick abackup.mode, a backup.provider, complete the storage setup below, then set backup.enabled: true. Neither mode has been exercised against a live install of this template version — rehearse on a throwaway release before relying on it.
postgres, which the Supabase image does not keep as a superuser, and pg_dumpall reads cluster-wide role data. Whether the dump completes on this image has not been verified — after enabling it, check the first run’s output with cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-backup"}' --limit 50 --since 10m and confirm an archive landed in the bucket.
- AWS S3
- Google Cloud Storage
Create a bucket
backup.aws.bucket and backup.aws.region to match.Set up a Cloud Account
backup.aws.cloudAccountName to its name.Create a bucket-scoped IAM policy
YOUR_BUCKET_NAME) and set backup.aws.policyName to its name. The template attaches it to the shared identity:Restoring a Backup
Logical. The archive ispg_dumpall output: a whole-cluster SQL script that recreates roles and databases, so it is meant for a fresh release installed with the same prerequisite secrets. Download it from your bucket (aws s3 cp or gsutil cp), forward the new Postgres port, and stream the script into psql on localhost:
already exists errors for the roles and databases the Supabase image creates on first boot — the rest of the script continues past them. Review the output before pointing clients at the restored release.
WAL-G. The sidecar’s backup list is available with wal-g backup-list inside the wal-g-backup container of RELEASE_NAME-postgres. A WAL-G restore (wal-g backup-fetch into an empty data directory, then starting Postgres against it) needs a fresh volume set and a one-off workload that this template does not provide, and it has not been verified here. Follow the WAL-G documentation and, before re-enabling backups on the restored cluster, set a new backup.aws.prefix or backup.gcp.prefix so it does not collide with the original cluster’s WAL stream.
Rotating a Credential
The prerequisite secrets live outside Helm, so changing one does nothing by itself — running workloads keep the value they booted with. Update the secret by applying its full manifest, then force a redeployment of every workload that reads it:-
Postgres password — change it on the roles first, then update the secret to match, or every service fails to authenticate. The same password is set on
postgres,supabase_admin,authenticator,supabase_auth_adminandsupabase_storage_admin;supabase_admin’s current password is already in the container’s environment:This command has not been run against a live install of this template — try it on a throwaway release first. - JWT values on the Postgres workload — the database keeps the JWT settings it was initialized with; a redeployment does not re-run the init scripts.
Upgrading from 1.0.0
Template version 1.1.0 moved every credential out of values and into prerequisite secrets. These keys no longer exist and are ignored if you still pass them:jwt.secret, jwt.anonKey, jwt.serviceRoleKey, jwt.secretKeyBase, postgres.password, postgres.database, studio.password, auth.smtp.password, storage.gcs.accessKeyId, storage.gcs.secretAccessKey and auth.providers.*.clientSecret. Before upgrading:
- Create the secrets from Prerequisites holding your current values — the Postgres password and the JWT settings are baked into the initialized database, so new values need the rotation procedure, not just an upgrade.
- Set
jwt.secretName,postgres.credentialsSecretName,studio.passwordSecretNameand, where used,auth.smtp.passwordSecretName,storage.gcs.credentialsSecretNameandauth.providers.*.clientSecretNameto those names, and remove the old keys from your values. - If you never changed 1.0.0’s default JWT values, rotate them: they were Supabase’s publicly published demo keys, so every default install shared one known
serviceRoleKey.
- The Studio dashboard login moved to Kong — reach Studio at
/on the Kong address, not on the Studio workload.studio.internalAccess.typenow defaults toworkload-list(Kong only), andstudio.allowedCidrshas become a login bypass; clear it unless you understand the consequence. realtime.minReplicasandrealtime.maxReplicaswere removed; Realtime is a single replica.storage.backend: s3gainedendpoint,forcePathStyleandcredentialsSecretNamefor S3-compatible stores; the defaultstorage.s3.bucketplaceholder was renamed andstorage.resources.minCpurose to125mso thelocalbackend can install.
Upgrading from 1.1.0
Template version 1.1.1 removes the AWS-managedReadOnlyAccess policy from the backup identity. That policy granted read access to every bucket in your AWS account and contained no write actions, so it never carried the backup itself — but it was silently supplying any read action your own bucket-scoped policy omitted. Update your IAM policy to the full action list in Backing Up before upgrading; if it already matches, no action is needed. The identity now carries cpln-connector and your bucket-scoped policies only. Nothing else changes.
The first cpln helm upgrade after an install can restart the Postgres workload even when no database value changed, so run it during a quiet period.
Scaling and Availability
Kong, PostgREST, Auth and Storage (with thes3 or gcs backend) are stateless and autoscale on CPU between their minReplicas and maxReplicas. Realtime, Studio, PgBouncer and Storage with the local backend run as single replicas. Postgres is a single stateful instance with no HA option in this template: a restart or upgrade is a brief interruption, and its data persists on RELEASE_NAME-postgres-vs across redeploys under the same release name. Scale Postgres vertically through postgres.resources and grow the volume with postgres.volumeset.capacity or its autoscaling block.
Troubleshooting
Workloads never become ready and cpln logs shows nothing
Workloads never become ready and cpln logs shows nothing
cpln logs returns no lines for them.Cause: A prerequisite secret named in your values does not exist, so the container never starts.Fix: Read status.versions[].message in cpln workload get-deployments RELEASE_NAME-kong --gvc GVC_NAME -o yaml (or the affected workload) — it names the missing secret. Create it as shown in Prerequisites; the workload recovers on its own, or immediately after cpln workload force-redeployment RELEASE_NAME-kong --gvc GVC_NAME.Every API call fails with bad_jwt
Every API call fails with bad_jwt
bad_jwt or invalid JWT from PostgREST, GoTrue or Storage.Cause: anonKey or serviceRoleKey is not an HMAC-SHA256 JWT signed by the secret in the same dictionary secret — typically a key copied from elsewhere or a rotated secret without re-minted keys.Fix: Re-mint all four values together with the snippet in Prerequisites, apply the updated secret, and force-redeploy the six workloads listed under Rotating a Credential.Admin request returns 403 You cannot consume this service
Admin request returns 403 You cannot consume this service
/auth/v1/admin/... or /pg/... is refused with 403 You cannot consume this service.Cause: The request carries anonKey. Kong admits only the serviceRoleKey consumer to the admin routes.Fix: Send serviceRoleKey in the apikey header from trusted server-side code only. This is the intended behavior for the public key.Render fails with credentialsSecretName is required when storage.s3.endpoint is set
Render fails with credentialsSecretName is required when storage.s3.endpoint is set
supabase: storage.s3.credentialsSecretName is required when storage.s3.endpoint is set.Cause: You pointed storage.s3.endpoint at an S3-compatible store without naming the access-key secret. Keyless cloud identity is AWS-only.Fix: Create the dictionary secret (accessKeyId, secretAccessKey) shown under Storage and set storage.s3.credentialsSecretName to it.Services fail with password authentication failed after changing the Postgres password
Services fail with password authentication failed after changing the Postgres password
password authentication failed for user.Cause: The password in the secret is applied only when the data directory is first initialized. The roles in the running database still have the old password.Fix: Change the password on the roles with ALTER ROLE as shown under Rotating a Credential, then force-redeploy the consumers — or put the old value back in the secret and redeploy.Realtime WebSocket handshake returns 403 TenantNotFound
Realtime WebSocket handshake returns 403 TenantNotFound
/realtime/v1/ handshake with 403 TenantNotFound.Cause: You are on template version 1.0.0, which shipped Realtime without seeding its tenant.Fix: Upgrade to 1.1.1 following Upgrading from 1.0.0; Realtime seeds its own tenant on boot from 1.1.0 onward.Studio opens without asking for a password
Studio opens without asking for a password
studio.allowedCidrs or a widened studio.internalAccess.type — instead of through Kong. Studio enforces no login of its own.Fix: Clear studio.allowedCidrs, leave studio.internalAccess.type: workload-list, and reach the dashboard at / on the Kong address, where basic auth applies.Important Notes
- Create the three prerequisite secrets before installing — JWT keys, Postgres credentials and the Studio password; a missing one wedges the deployment silently.
serviceRoleKeybypasses row-level security — server-side only, never in client code; it is also the only key Kong admits to/auth/v1/admin/and/pg/.- Keep the JWT keys and the signing secret in sync —
anonKeyandserviceRoleKeymust be signed bysecret, or every service returnsbad_jwt; mint them together. - The dashboard login only applies through Kong — leave
studio.allowedCidrsempty andstudio.internalAccess.typeatworkload-list; anything else exposes Studio, and theserviceRoleKeyit holds, without a password. - Publishing Kong publishes the dashboard too —
kong.publicAccess.enabled: trueputs Studio at/behind the Studio password, so make it a strong one. - Keep the
supabase/postgresimage — a stock Postgres image lacks the extensions GoTrue, PostgREST, Realtime and Storage require. - The database login role is
postgres— fixed by the Supabase image; there is no username knob. The image’s own superuser issupabase_admin, which the services use internally. - Changing a secret restarts nothing — force-redeploy the consumers after a rotation, and change the Postgres password on the roles before changing the secret.
- Realtime does not scale out — single replica by design; clients must handle a dropped socket and reconnect after it restarts.
- Storage and backup buckets must be separate — each with its own bucket, Cloud Account and IAM policy.
- Switching
backup.moderestarts Postgres — the two modes need different startup flags. - Access changes are not instant — a change to
kong.publicAccessorstudio.allowedCidrstakes a little while to take effect; re-test rather than assuming it failed. - Data survives redeploys under the same release name — the database on
RELEASE_NAME-postgres-vs, local files onRELEASE_NAME-storage-vs;cpln helm uninstall RELEASE_NAME --gvc GVC_NAMEdeletes both.