SaaS Starter
Deploy

Supabase

Deploy this boilerplate against Supabase Postgres + Supabase Storage with the smallest possible config diff.

Supabase is a first-class deployment target for this boilerplate. You get Postgres + an S3-compatible storage bucket from a single vendor, and the boilerplate's existing Prisma + storage abstractions plug in without any code changes — only env vars.

This page covers the recommended path: Supabase as the database and file-storage backend, with BetterAuth running on top of Prisma against the Supabase Postgres instance. We do not use Supabase Auth — BetterAuth already owns the auth flows, sessions, and OAuth wiring in this codebase.

Out of scope: Supabase Realtime and Edge Functions. The boilerplate ships its own Socket.IO + BullMQ stack for those concerns.

1. Create a Supabase project

  1. Sign in at supabase.com and create a new project.
  2. Pick a strong database password (you will need it in step 2).
  3. Wait for the project to provision (~2 minutes).

2. Grab the connection strings

Open Project Settings → Database. You need two connection strings:

VariableSource (Supabase UI)Why
DATABASE_URL"Connection pooling" → Transaction modePooled (port 6543); used by the running app for short-lived queries
DIRECT_URL"Connection string" → URIDirect (port 5432); used by prisma migrate for advisory locks

Append ?pgbouncer=true&connect_timeout=15 to DATABASE_URL so Prisma knows it is talking to a pooler.

# .env (server)
DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres?pgbouncer=true&connect_timeout=15"
DIRECT_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres"

Why two URLs? Supabase's pgbouncer pooler strips advisory locks and long-lived sessions, both of which Prisma migrations rely on. The runtime uses the pooled URL for connection efficiency; migrations use the direct URL. The prisma/schema.prisma datasource db block already declares directUrl = env("DIRECT_URL") for this reason.

3. Run migrations

From the repo root:

bunx prisma migrate deploy --schema apps/server/prisma/schema.prisma

Prisma will use DIRECT_URL automatically. Verify the tables landed via Table Editor in the Supabase UI.

4. (Optional) Switch storage to Supabase

If you want avatar uploads and any future file uploads to go to Supabase Storage instead of S3 / Cloudflare R2 / local disk:

  1. Create a bucket. In Storage → New bucket, name it (e.g. avatars). Mark it "Public" if you want browser-fetchable URLs without signing.

  2. Set a bucket policy. Public buckets need a SELECT policy granting anon read access to objects. Private buckets do not — the adapter mints short-lived signed URLs via presignDownload.

    Example public-read policy (run in SQL Editor):

    create policy "public read avatars"
      on storage.objects for select
      to public
      using ( bucket_id = 'avatars' );
  3. Grab the service-role key. Project Settings → API → Service role. Treat it like a database password — server-only, never the browser.

  4. Set env vars.

    STORAGE_PROVIDER=supabase
    SUPABASE_URL=https://<ref>.supabase.co
    SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
    SUPABASE_STORAGE_BUCKET=avatars
  5. Restart the server. The storage adapter selector in apps/server/src/modules/storage/infrastructure/providers/index.ts picks up STORAGE_PROVIDER=supabase and instantiates the Supabase adapter; if any required var is missing, it logs a warning and falls back to the no-op null provider so the boot sequence still completes.

5. Verify

  • GET /healthz should return 200.
  • GET /readyz should report database: ok and storage: ok (the storage check pings the bucket via list(limit=1)).
  • Trigger an avatar upload from the client; the file appears under Storage → <bucket> in Supabase.

What we deliberately skipped

  • Supabase Auth. BetterAuth manages sessions, OAuth, and magic-link in this boilerplate. Stacking Supabase Auth on top would duplicate that surface. BetterAuth talks to Supabase Postgres via Prisma like any other Postgres deployment — no special wiring.
  • Realtime. Use the boilerplate's Socket.IO server (already wired with optional Redis adapter for horizontal scaling).
  • Edge Functions. Background work belongs in BullMQ workers (apps/server/src/bootstrap/worker.ts).

Troubleshooting

  • prisma migrate deploy hangs or errors with "advisory lock" — you are pointing migrations at the pooler. Confirm DIRECT_URL uses port 5432 (not 6543).
  • Avatar upload returns 500 with bucket not found — the SUPABASE_STORAGE_BUCKET value does not match a bucket in the project, or the service-role key belongs to a different project.
  • Public URL returns 403 — bucket is private and you have not added a SELECT policy. Either add the policy (above) or switch the client to fetch via the signed URL emitted by presignDownload.

On this page