Skip to main content

Overview

Supabase is an open-source backend-as-a-service built on PostgreSQL. This template deploys the full self-hosted Supabase stack on Control Plane — all services run in your own GVC with no dependency on Supabase Cloud.

Architecture

  • Postgres — Supabase-patched PostgreSQL 15 with pgvector, pg_graphql, pg_net, pgjwt, and other required extensions pre-installed.
  • Kong — API gateway and single public entry point. Routes all traffic to PostgREST, Auth, Realtime, and Storage.
  • PostgREST — Auto-generated REST and GraphQL API derived from your Postgres schema. Stateless, scales horizontally.
  • Auth (GoTrue) — Full-featured auth service supporting email/password, magic links, OAuth providers, and JWT sessions. Stateless, scales horizontally.
  • Realtime — WebSocket server that streams database change events to subscribed clients. Stateless, scales horizontally.
  • Storage — Object storage API backed by S3, GCS, or a local volume.
  • Studio — Web dashboard for managing your database, auth users, storage, and API settings.
  • pg_meta — Postgres metadata API. Runs as a sidecar inside the Studio workload.
  • PgBouncer (optional) — Connection pooler that multiplexes application connections into a smaller pool of real database connections.
  • Backup (optional) — Logical (pg_dump cron) or WAL-G (continuous WAL archiving with PITR support).

What Gets Created

  • Stateful Postgres Workload — Supabase-patched PostgreSQL with a persistent volume set.
  • Standard Kong Workload — API gateway, autoscales on RPS.
  • Standard PostgREST Workload — REST/GraphQL API, autoscales on RPS.
  • Standard Auth Workload — GoTrue auth service, autoscales on RPS.
  • Standard Realtime Workload — WebSocket change feed, autoscales on RPS.
  • Standard Storage Workload — Object storage API, autoscales on RPS (optional, enabled by default).
  • Standard Studio Workload — Web dashboard with pg_meta sidecar (optional, enabled by default).
  • Standard PgBouncer Workload — Connection pooler (optional, disabled by default).
  • Cron Backup Workload (optional) — Logical or WAL-G backup on a schedule.
  • Volume Sets — One for Postgres data; one for Storage when using the local backend.
  • Identity & Policy — Identities bound to each workload with reveal access to credential secrets, and cloud storage access when backup or S3/GCS storage is enabled.
  • Secrets — Dictionary secrets for Postgres credentials, JWT keys, and service API keys; opaque secrets for Kong routing config, SMTP settings, and OAuth provider credentials.
This template does not create a GVC. You must deploy it into an existing GVC.

Prerequisites

This template has no external prerequisites unless S3/GCS storage or backup is enabled. To install, follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations
Pulumi Icon Streamline Icon: https://streamlinehq.com

Pulumi

Declare templates in your Pulumi programs

Configuration

The default values.yaml for this template:
postgres:
  image: supabase/postgres:15.8.1.060
  password: change-me-postgres
  database: postgres
  resources:
    minCpu: 500m
    minMemory: 512Mi
    maxCpu: 2
    maxMemory: 2Gi
  volumeset:
    capacity: 10  # initial capacity in GiB (minimum is 10)
    autoscaling:
      enabled: false
      maxCapacity: 100
      minFreePercentage: 10
      scalingFactor: 1.2
  internalAccess:
    type: same-gvc  # options: none, same-gvc, same-org, workload-list
    workloads:
      #- //gvc/GVC_NAME/workload/WORKLOAD_NAME

kong:
  image: kong:2.8.1
  resources:
    minCpu: 100m
    minMemory: 256Mi
    maxCpu: 1000m
    maxMemory: 1Gi
  minReplicas: 1
  maxReplicas: 3
  publicAccess:
    enabled: false
    siteUrl: ""  # e.g. https://api.my-app.com — required when publicAccess is enabled

postgrest:
  image: postgrest/postgrest:v12.2.3
  resources:
    minCpu: 100m
    minMemory: 128Mi
    maxCpu: 500m
    maxMemory: 512Mi
  minReplicas: 1
  maxReplicas: 3

auth:
  image: supabase/gotrue:v2.170.0
  resources:
    minCpu: 100m
    minMemory: 128Mi
    maxCpu: 500m
    maxMemory: 512Mi
  minReplicas: 1
  maxReplicas: 3
  disableSignup: false
  smtp:
    enabled: false
    host: smtp.example.com
    port: 587
    user: smtp-user
    password: smtp-password
    senderName: Supabase
    senderEmail: noreply@example.com
  # providers:
  #   github:
  #     clientId: ""
  #     clientSecret: ""
  #   google:
  #     clientId: ""
  #     clientSecret: ""

realtime:
  enabled: true
  image: supabase/realtime:v2.34.47
  resources:
    minCpu: 100m
    minMemory: 128Mi
    maxCpu: 500m
    maxMemory: 512Mi
  minReplicas: 1
  maxReplicas: 3

storage:
  enabled: true
  image: supabase/storage-api:v1.14.6
  resources:
    minCpu: 100m
    minMemory: 128Mi
    maxCpu: 500m
    maxMemory: 512Mi
  minReplicas: 1
  maxReplicas: 3
  backend: s3  # options: local, s3, gcs
  volumeset:   # only used when backend is local
    capacity: 10
    autoscaling:
      enabled: false
      maxCapacity: 100
      minFreePercentage: 10
      scalingFactor: 1.2
  s3:          # only used when backend is s3
    bucket: my-storage-bucket
    region: us-east-1
    cloudAccountName: my-s3-cloudaccount
    policyName: my-storage-policy
  gcs:         # only used when backend is gcs (S3-compatible HMAC auth, no cloud account needed)
    bucket: my-storage-bucket
    accessKeyId: my-gcs-hmac-access-key-id
    secretAccessKey: my-gcs-hmac-secret-access-key

studio:
  enabled: true
  image: supabase/studio:2025.06.02-sha-8f2993d
  username: supabase
  password: change-me-studio
  resources:
    minCpu: 100m
    minMemory: 256Mi
    maxCpu: 500m
    maxMemory: 512Mi
  allowedCidrs: []
  #  - 203.0.113.0/24
  #  - 0.0.0.0/0
  internalAccess:
    type: same-gvc
  meta:
    image: supabase/postgres-meta:v0.86.0
    resources:
      minCpu: 100m
      minMemory: 64Mi
      maxCpu: 200m
      maxMemory: 256Mi

jwt:
  secret: your-super-secret-jwt-token-with-at-least-32-characters-long
  secretKeyBase: your-super-secret-key-base-used-by-realtime-must-be-at-least-64-characters-long!!
  anonKey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  serviceRoleKey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

pgbouncer:
  enabled: false
  image: edoburu/pgbouncer:v1.25.1-p0
  poolMode: transaction  # options: session, transaction, statement
  defaultPoolSize: 25
  maxClientConn: 1000
  replicas: 1
  resources:
    minCpu: 100m
    minMemory: 64Mi
    maxCpu: 200m
    maxMemory: 128Mi

backup:
  enabled: false
  mode: logical  # options: logical, walg
  provider: aws  # options: aws, gcp
  resources:
    minCpu: 100m
    minMemory: 128Mi
    maxCpu: 200m
    maxMemory: 256Mi
  logical:
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0
    schedule: "0 2 * * *"  # daily at 2am UTC
  walg:
    intervalSeconds: 21600  # base backup every 6 hours
  aws:
    bucket: my-supabase-backup-bucket
    region: us-east-1
    cloudAccountName: my-backup-cloudaccount
    policyName: my-backup-policy
    prefix: supabase/backups
  gcp:
    bucket: my-supabase-backup-bucket
    cloudAccountName: my-backup-cloudaccount
    prefix: supabase/backups

JWT Keys

Supabase uses JWT to authenticate requests between services and from clients.
  • jwt.secret — The signing secret for all JWTs. Must be at least 32 characters.
  • jwt.secretKeyBase — Used by Realtime (Phoenix) for cookie signing. Must be at least 64 characters. Generate with openssl rand -base64 64.
  • jwt.anonKey — Public key for unauthenticated (anonymous) client access.
  • jwt.serviceRoleKey — Privileged key that bypasses row-level security. For trusted server-side code only.
The default values are Supabase’s official published development keys and work together out of the box. Change all of them before any production deployment.
anonKey and serviceRoleKey must be valid HMAC-SHA256 JWTs signed with jwt.secret. Using mismatched keys causes bad_jwt errors across all services. Use the Supabase key generator to produce a matching set.

Kong (API Gateway)

Kong is the single entry point for all Supabase API traffic. PostgREST, Auth, Realtime, and Storage are only reachable through Kong.
  • kong.publicAccess.enabled — Expose Kong on a public external endpoint.
  • kong.publicAccess.siteUrl — The full URL clients will reach Kong at (e.g. https://api.my-app.com). Required when publicAccess is enabled. GoTrue uses this for OAuth redirect callbacks and magic link emails.
When publicAccess is disabled, internal clients use the Kong hostname within the GVC and OAuth/magic links will not work.

Postgres

You must use the supabase/postgres image. The standard postgres image is missing required extensions (pgvector, pg_graphql, pg_net, pgjwt, etc.) that GoTrue, PostgREST, Realtime, and Storage depend on.
  • postgres.password — Database superuser password. Change before deploying to production.
  • postgres.resources — CPU and memory limits/requests for the Postgres workload.
  • postgres.volumeset.capacity — Initial volume size in GiB (minimum 10). Autoscaling is configurable.
  • postgres.internalAccess.type — Controls which workloads can reach Postgres directly: same-gvc, same-org, workload-list, or none. All Supabase services connect internally. Use workload-list to also grant access to your own application workloads.

Auth (GoTrue)

  • auth.disableSignup — Set to true to prevent new user registration.
SMTP — required for magic link login, email confirmation, and password reset:
  • auth.smtp.enabled — Enable SMTP. When disabled, all signups are auto-confirmed and email flows are unavailable.
  • auth.smtp.host / auth.smtp.port — SMTP server address and port.
  • auth.smtp.user / auth.smtp.password — SMTP credentials.
  • auth.smtp.senderName / auth.smtp.senderEmail — Display name and from address for outgoing emails.
Any SMTP provider works: Gmail, SendGrid, Mailgun, Postmark, Mailtrap, etc. OAuth Providers — enable by uncommenting auth.providers and adding credentials:
auth:
  providers:
    github:
      clientId: ""
      clientSecret: ""
    google:
      clientId: ""
      clientSecret: ""
Supported providers: Apple, Azure, Bitbucket, Discord, Facebook, Figma, GitHub, GitLab, Google, Kakao, Keycloak, LinkedIn, Notion, Slack, Spotify, Twitch, Twitter/X, WorkOS, Zoom. When setting up OAuth in your provider’s developer console:
  • Authorized JavaScript Origin: {kong.publicAccess.siteUrl}
  • Authorized Redirect URI: {kong.publicAccess.siteUrl}/auth/v1/callback
OAuth requires kong.publicAccess.enabled: true and a valid siteUrl. OAuth providers will not redirect to internal hostnames.

Storage

Three backends are supported:
BackendDescription
s3Stateless, horizontally scalable. Recommended for production.
gcsGCS accessed via the S3-compatible API using HMAC keys. No Cloud Account needed.
localStateful, single-replica, volume-backed. For development only.
For s3: 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 gcs: create HMAC keys in the GCP console under Cloud Storage → Settings → Interoperability and set storage.gcs.accessKeyId and storage.gcs.secretAccessKey.
If using S3/GCS for backup as well, storage and backup require separate buckets, cloud accounts, and IAM policies.

Studio (Web Dashboard)

Studio is protected by username and password login and has no external access by default.
  • studio.username / studio.password — Login credentials. Change before deploying to production.
  • studio.allowedCidrs — List of CIDRs allowed to reach Studio externally. Empty by default (no external access). Use cpln workload connect for local tunnel access, or set to ["0.0.0.0/0"] to open it publicly (login is still required).

PgBouncer

PgBouncer multiplexes application connections into a smaller pool of real database connections, reducing Postgres connection overhead under high concurrency. Disabled by default.
Pool ModeDescription
transactionConnection held only for the duration of a transaction. Best for most web and API workloads. Not compatible with SET variables, temporary tables, or advisory locks.
sessionConnection held for the entire client session. Compatible with all Postgres features.
statementConnection returned after every statement. Transactions not supported.
  • pgbouncer.defaultPoolSize — Number of real Postgres connections per pool (default: 25).
  • pgbouncer.maxClientConn — Maximum number of client connections PgBouncer accepts (default: 1000).
When enabled, connect through PgBouncer rather than directly to Postgres.

Connecting

All API traffic flows through Kong. Connect your application using the Kong endpoint:
EndpointAddress
API — internal{release-name}-kong.{gvc}.cpln.local:8000
API — public{kong.publicAccess.siteUrl} (when publicAccess enabled)
Postgres — direct{release-name}-postgres.{gvc}.cpln.local:5432
Postgres — via PgBouncer{release-name}-pgbouncer.{gvc}.cpln.local:5432 (when enabled)
Key API paths routed through Kong:
ServicePath
PostgREST (REST API)/rest/v1/
Auth (GoTrue)/auth/v1/
Storage/storage/v1/
Realtime/realtime/v1/
Pass apikey: {anonKey} as a header on all requests. Use serviceRoleKey for privileged server-side calls. The Supabase client library handles auth headers automatically:
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://api.my-app.com',  // kong.publicAccess.siteUrl
  'YOUR_ANON_KEY'
)

Backing Up

Two backup modes are available:
ModeMechanismBest For
logicalpg_dump cronPortable SQL dumps, smaller databases, cross-version migrations
walgContinuous WAL archiving + base backupsProduction — supports point-in-time recovery (PITR)
Set backup.enabled: true, choose a mode and provider, then fill in the corresponding provider block.
Switching backup.mode between logical and walg changes Postgres archive_mode and wal_level, which requires a Postgres restart. Plan the switch accordingly.

AWS S3

1

Create a bucket

Create an S3 bucket. 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. Set backup.aws.cloudAccountName to its name.
3

Create an IAM policy

Create an IAM policy with the following JSON (replace YOUR_BUCKET_NAME) and set backup.aws.policyName to its name:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
        }
    ]
}

GCS

1

Create a bucket

Create a GCS bucket. Set backup.gcp.bucket to its name.
2

Set up a Cloud Account

If you do not have one, create a Cloud Account. Set backup.gcp.cloudAccountName to its name.
You must add the Storage Admin role to the GCP service account created for the Cloud Account.

Restoring a Backup

Logical Restore

Run from a machine with network access to Postgres (e.g. via cpln workload connect): AWS S3:
export PGPASSWORD="YOUR_POSTGRES_PASSWORD"

aws s3 cp "s3://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql \
      --host={release-name}-postgres.{gvc}.cpln.local \
      --port=5432 \
      --username=postgres \
      --dbname=postgres

unset PGPASSWORD
GCS:
export PGPASSWORD="YOUR_POSTGRES_PASSWORD"

gsutil cp "gs://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql \
      --host={release-name}-postgres.{gvc}.cpln.local \
      --port=5432 \
      --username=postgres \
      --dbname=postgres

unset PGPASSWORD

WAL-G Restore

WAL-G restores require an empty data directory.
1

List available backups

cpln workload connect {release-name}-postgres --gvc {gvc} --container wal-g-backup -- wal-g backup-list
2

Stop the Postgres workload

Stop the workload via the Control Plane console or CLI.
3

Create a new volume set

Create a fresh empty volume set for the restore target — do not reuse the existing one.
4

Run the restore

Run a one-off workload with the new volume set mounted at /var/lib/postgresql/data and execute:
wal-g backup-fetch /var/lib/postgresql/data/pg_data BACKUP_NAME
5

Restart Postgres

Re-point the Postgres workload to the restored volume set and restart. After restore, update backup.aws.prefix (or backup.gcp.prefix) to a new path before re-enabling backups to avoid WAL stream conflicts with the original cluster’s archived segments.

Important Notes

  • Use the Supabase Postgres imagesupabase/postgres is required. The standard postgres image is missing extensions that GoTrue, PostgREST, Realtime, and Storage depend on.
  • JWT keys must matchanonKey and serviceRoleKey must be HMAC-SHA256 JWTs signed with jwt.secret. Mismatched keys produce bad_jwt errors. Use the official Supabase key generator to produce a matching set.
  • Change default credentials before productionjwt.secret, postgres.password, and studio.password are placeholders. Replace all of them before any production deployment.
  • OAuth requires a public siteUrl — Set kong.publicAccess.enabled: true and kong.publicAccess.siteUrl before configuring any OAuth provider.
  • Storage and backup use separate buckets — Do not share a bucket between storage.s3 and backup.aws. Each requires its own bucket, cloud account, and IAM policy.
  • Studio has no external access by default — Use cpln workload connect for local access, or set studio.allowedCidrs to open it externally.

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

GoTrue (Auth) Documentation

GoTrue auth service configuration and API reference

PostgREST Documentation

Auto-generated REST API from your Postgres schema

WAL-G Documentation

WAL-G continuous archiving and PITR restore documentation

Generate Supabase API Keys

Generate matching JWT secret, anonKey, and serviceRoleKey