Skip to main content

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 public anonKey reaches the API routes while the admin routes and the dashboard login are restricted.
This template deploys into an existing GVC that you already have — it does not create one.

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.
1

JWT signing secret and API keys

A dictionary secret with exactly the 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:
Set 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.
2

Postgres credentials

A dictionary secret with exactly the keys password and database. The login role name is fixed at postgres by the Supabase image, so it is not part of the secret:
Set postgres.credentialsSecretName to this name.
3

Studio dashboard password

Required while 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:
Set studio.passwordSecretName to this name.
4

File storage bucket (default install)

The default 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.
5

SMTP password (optional)

Only when auth.smtp.enabled: true. An opaque secret with --encoding plain holding the SMTP password:
Set auth.smtp.passwordSecretName to this name.
6

OAuth client secrets (optional)

One opaque secret per provider, holding only the client secret the provider issued:
Set auth.providers.github.clientSecretName (or auth.providers.google.clientSecretName) to this name — see Auth.
Backup buckets are optional and separate from the storage bucket — see Backing Up.
Create the secrets before installing. A name that points at a secret which does not exist installs “successfully” and then wedges silently: cpln logs shows nothing, and the only place the missing secret is named is status.versions[].message in cpln workload get-deployments RELEASE_NAME-kong --gvc GVC_NAME -o yaml (check the other workloads the same way). The workloads recover on their own once the secret exists, or immediately after cpln workload force-redeployment RELEASE_NAME-kong --gvc GVC_NAME.

Installation

A default install needs the three secret names plus the S3 storage bucket details:
Replace the four storage.s3.* lines with --set storage.enabled=false to install without file storage. Or install using your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

Postgres

  • postgres.image — Must stay a supabase/postgres tag. GoTrue, PostgREST, Realtime and Storage depend on extensions (pgvector, pg_graphql, pg_net, pgjwt) that the stock postgres image does not ship.
  • postgres.credentialsSecretName — The dictionary secret from Prerequisites. The login role is always postgres; the same password is applied to the internal service roles on first boot.
  • postgres.internalAccess.type — Who can reach Postgres directly on port 5432. All Supabase services connect inside the GVC; use workload-list with workloads to 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 when studio.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 to RELEASE_NAME-kong, or the workload’s canonical *.cpln.app endpoint. With public access off, the template falls back to the internal Kong hostname and email or OAuth links only work inside the GVC.
Kong is also where authorization happens. Requests are identified by the apikey header they carry:

PostgREST

Stateless; autoscales on CPU between 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 to kong.publicAccess.siteUrl and the redirect URI to SITE_URL/auth/v1/callback. OAuth needs kong.publicAccess.enabled: true — providers will not redirect to internal hostnames.

Realtime

Realtime runs as a single replica and has no replica knobs: the image seeds its own tenant row on every boot, so a second replica would delete the row the first one is serving. Disable it if your app has no realtime subscriptions.

Storage

For an S3-compatible store or GCS, create the access key pair on the provider (scoped to the one bucket: read, write and delete objects, list the bucket) and put it in a dictionary secret before installing:
Then set 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

The dashboard login is enforced by Kong’s basic-auth plugin on the / 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

PgBouncer is a pooler for your application workloads; the Supabase services keep connecting to Postgres directly. 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

See Backing Up for the bucket, Cloud Account and IAM setup and what each mode produces.

Connecting

All API traffic goes through Kong. Read the public address from status.canonicalEndpoint in cpln workload get RELEASE_NAME-kong --gvc GVC_NAME -o yaml. API paths through Kong: /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:
To verify the gateway from your machine without publishing it, forward Kong’s port and call the unauthenticated health route:

First Run

1

Wait for the gateway to report ready

Postgres initializes first; Storage and Realtime may restart a few times while it does, then settle. Kong becomes ready once GoTrue answers its health check:
2

Open the dashboard

With the port-forward above running, browse to 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.
3

Publish the gateway

Attach a custom domain to 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.
4

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 a backup.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. The logical job connects as 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.
Switching backup.mode between logical and walg changes Postgres’s wal_level and archive_mode startup flags, which restarts the database. Plan the switch accordingly.
1

Create a bucket

Create an S3 bucket — not the one Storage uses. Set backup.aws.bucket and backup.aws.region to match.
2

Set up a Cloud Account

If you do not have one, create a Cloud Account for your AWS account. Set backup.aws.cloudAccountName to its name.
3

Create a bucket-scoped IAM policy

Create an IAM policy with the JSON below (replace YOUR_BUCKET_NAME) and set backup.aws.policyName to its name. The template attaches it to the shared identity:

Restoring a Backup

These procedures were written against the chart’s configuration and the backup image’s output format. They have not been exercised against a live install of this template — rehearse on a throwaway release, and treat any step that errors as a stop. An invented restore is worse than none, so what cannot be traced to the chart is left out.
Logical. The archive is pg_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:
Expect 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:
Two values are consumed only once, when the Postgres data directory is first initialized, so rotating them later does not reach what the database already stores:
  • 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_admin and supabase_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:
  1. 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.
  2. Set jwt.secretName, postgres.credentialsSecretName, studio.passwordSecretName and, where used, auth.smtp.passwordSecretName, storage.gcs.credentialsSecretName and auth.providers.*.clientSecretName to those names, and remove the old keys from your values.
  3. 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.
Other changes in 1.1.0 that affect an existing install:
  • The Studio dashboard login moved to Kong — reach Studio at / on the Kong address, not on the Studio workload. studio.internalAccess.type now defaults to workload-list (Kong only), and studio.allowedCidrs has become a login bypass; clear it unless you understand the consequence.
  • realtime.minReplicas and realtime.maxReplicas were removed; Realtime is a single replica.
  • storage.backend: s3 gained endpoint, forcePathStyle and credentialsSecretName for S3-compatible stores; the default storage.s3.bucket placeholder was renamed and storage.resources.minCpu rose to 125m so the local backend can install.

Upgrading from 1.1.0

Template version 1.1.1 removes the AWS-managed ReadOnlyAccess 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 the s3 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

Symptom: After install, one or more workloads stay not ready and 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.
Symptom: Requests through Kong return 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.
Symptom: A call to /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.
Symptom: Install or upgrade stops with 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.
Symptom: After updating the Postgres credentials secret, Auth, PostgREST, Storage or Studio log 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.
Symptom: Subscriptions never open; the Realtime workload answers every /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.
Symptom: The dashboard loads with no login prompt.Cause: You reached the Studio workload directly — through 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.
  • serviceRoleKey bypasses 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 — anonKey and serviceRoleKey must be signed by secret, or every service returns bad_jwt; mint them together.
  • The dashboard login only applies through Kong — leave studio.allowedCidrs empty and studio.internalAccess.type at workload-list; anything else exposes Studio, and the serviceRoleKey it holds, without a password.
  • Publishing Kong publishes the dashboard too — kong.publicAccess.enabled: true puts Studio at / behind the Studio password, so make it a strong one.
  • Keep the supabase/postgres image — 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 is supabase_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.mode restarts Postgres — the two modes need different startup flags.
  • Access changes are not instant — a change to kong.publicAccess or studio.allowedCidrs takes 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 on RELEASE_NAME-storage-vs; cpln helm uninstall RELEASE_NAME --gvc GVC_NAME deletes both.

External References

Supabase Self-Hosting Guide

Official self-hosting documentation and architecture overview

Supabase Client Libraries

JavaScript, Python, Swift, Kotlin and other client SDK references

Supabase Auth

Email, magic link and OAuth sign-in with GoTrue

PostgREST Documentation

The REST API served from your Postgres schema

WAL-G Documentation

Continuous archiving, base backups and restore

Supabase Template

View the source files, default values, and chart definition