MainSDK
Search⌘K
Integrations

Supabase

Use Supabase for MainSDK auth, data, real-time updates and backups - project setup, main.yaml per interface, secrets, local testing and swapping.

4 interfaces4 driversWritten guide
Updated 2026-10-28

Supabase is a hosted Postgres platform that bundles sign-in, real-time channels and backups with the database. In MainSDK it can serve four interfaces: auth through Supabase Auth, db on the project's Postgres database, realtime through Supabase Realtime, and backup through Supabase point-in-time recovery. Pick it when your app already runs on Supabase, or when you want one provider for users, rows and live updates. You can use any subset: each interface names its own driver.

Interfaces it serves

Pick Supabase in main.yaml for any of these. Your calls stay the same, and you can swap later.

Before you start

You need a Supabase account and one project per region you store data in.

  1. Create a project. You choose its region when you create it, and the project's database stays there. MainSDK stores each record in its owner's region (US, EU or India), so create one project for each region you serve.
  2. Copy the database connection string for each project. Supabase offers a direct connection and a pooled one; use the one that suits where your app runs.
  3. Copy the API keys from the project settings: a public key that is safe in the browser and a secret (service role) key that bypasses row-level security. The secret key belongs only on your server.
  4. For Auth: set the site URL and the allowed redirect URLs for each environment, and turn on the sign-in providers you want, such as GitHub.
  5. For Realtime: no extra setup for realtime channels beyond the project's keys.
  6. For backups: turn on point-in-time recovery for the project. It is an add-on on paid plans; check its current requirements in Supabase's docs.

Store the connection strings and keys in your secrets manager, never in code or a committed env file.

Configure

Sign-in:

main.yaml
auth:
  driver: supabase
  methods: [passkeys]
  # [to be decided] key names; suggested secrets: SUPABASE_URL, SUPABASE_SECRET_KEY

Data, one Supabase project per region:

main.yaml
data: { store: postgres, residency: { default: eu, by-market: { in: in, us: us } } }
db:
  driver: supabase
  regions:
    us: secret:SUPABASE_DB_US   # suggested name; the US project's connection string
    eu: secret:SUPABASE_DB_EU   # suggested name; the EU project's connection string
    in: secret:SUPABASE_DB_IN   # suggested name; the India project's connection string
  fields:
    user.email: { label: personal }

Real-time updates:

main.yaml
realtime:
  driver: supabase-realtime
  # [to be decided] key names; provider keys go in as secret: references

Backups follow the db driver, so point-in-time recovery comes from the Supabase project:

main.yaml
backup:
  rpo: 15m
  rto: 1h
  keep: 35d
  drill: { every: 30d, to: scratch }

Use it

Guard a route with Supabase Auth behind auth:

app/api/me/route.ts
import { auth } from "mainsdk";

export async function GET(req: Request) {
  const session = await auth.require(req);   // 401 with MAIN-AUTH-0001 if not signed in
  return Response.json(session.user);
}

Read records scoped to their owner, from the owner's region project:

app/api/orders/route.ts
import { auth, db } from "mainsdk";

export async function GET(req: Request) {
  const session = await auth.require(req);
  return Response.json(await db.orders.for(session.user).list({ limit: 20 }));
}

Push a live update over Supabase Realtime:

app/orders/actions.ts
"use server";
import { realtime } from "mainsdk";

export async function markShipped(orderId: string) {
  await realtime.publish(`order:${orderId}`, { status: "shipped" });
}

Prove your restore works with a drill into scratch:

scripts/drill.ts
import { runDrill } from "mainsdk/backup";

const result = await runDrill({ driver, declared, app: "my-app", at: new Date(), to: "scratch" });   // restores into scratch, never production

mainsdk drill restore --to scratch runs the same drill from the command line and signs the evidence.

Local development and testing

  • No Supabase project: mainsdk dev runs db on local SQLite and auth on the local driver, with no keys.
  • Supabase locally: the Supabase CLI can run the Supabase stack on your machine in Docker; point your local secrets at it to try the real drivers.
  • Tests: createTestApp() from @mainsdk/testing runs auth, db and realtime on in-memory fakes. chaos.on("db", { outage: true }) simulates the database being unreachable.
  • Backups: point-in-time recovery needs a real project, so the drill is your test. Run mainsdk drill restore --to scratch on a schedule in CI.
app/api/orders/orders.test.ts
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { db } from "mainsdk";

it("db.AC2 refuses another user's order", async () => {
  createTestApp({ main: "./main.yaml" });
  const order = await db.orders.for({ id: "u1" }).create({ total: 4900 });
  await assert.rejects(db.orders.for({ id: "u2" }).get(order.id), { code: "MAIN-DATA-0007" });
});

What MainSDK adds on top

  • No query without an owner scope, enforced by types, whether or not your tables have row-level security policies.
  • Each record stored in its owner's region; cross-region joins fail mainsdk check with MAIN-DATA-0013.
  • Short-lived, device-bound sessions and passkeys by default, failing closed when Supabase Auth is unreachable.
  • Channel access checked per user and tenant on every Realtime channel.
  • Declared RPO and RTO checked against the project's backup settings, and timed restore drills recorded as signed evidence.

See Sign-in and sessions, Data and records, Real-time updates and Backups and restore drills.

Limits and notes

  • The db spec's driver ticket covers Postgres, Neon and SQLite; the Supabase db driver is listed out of scope there and comes later.
  • The auth spec lists supabase among its drivers but has no build ticket for it yet.
  • Supabase Realtime for realtime has no spec yet.
  • Point-in-time recovery comes with the backup-restore-evidence spec (phase 3, milestone 1.2). MainSDK never runs or stores backups; Supabase does.
  • The secret key bypasses row-level security. Keep it on the server; MainSDK's owner scope still applies to every db query.
  • auth.raw() and db.raw() return the Supabase clients for features MainSDK does not wrap. mainsdk check lists every use.

Swap to or from it

Each interface swaps on its own line:

main.yaml
db:
  driver: neon   # was supabase

FAQ

Do I have to move everything to Supabase? No. Use Supabase for db and keep GitHub for auth, or the other way round. Each interface names its own driver.

Does MainSDK replace row-level security? No. Owner scope is enforced in your code by types; row-level security in the database is a second layer you can keep. Each driver declares whether it offers row-level security in its capability profile.

Why one project per region? A Supabase project's database lives in one region. MainSDK writes each owner's records to their region's store, so each region needs its own project.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet