Supabase
Use Supabase for MainSDK auth, data, real-time updates and backups - project setup, main.yaml per interface, secrets, local testing and swapping.
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.
authBuilding for 0.3Driver supabase Supabase Auth
auth:
driver: supabaseSwap to another provider
dbBuilding for 0.3Driver supabase Supabase
db:
driver: supabaseSwap to another provider
realtimeBuilding for 0.3Driver supabase-realtime Supabase Realtime
later
realtime:
driver: supabase-realtimeSwap to another provider
backupBuilding for 1.2Driver supabase-pitr Supabase point-in-time recovery
backup:
driver: supabase-pitrBefore you start
You need a Supabase account and one project per region you store data in.
- 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.
- 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.
- 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.
- 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.
- For Realtime: no extra setup for realtime channels beyond the project's keys.
- 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:
auth:
driver: supabase
methods: [passkeys]
# [to be decided] key names; suggested secrets: SUPABASE_URL, SUPABASE_SECRET_KEYData, one Supabase project per region:
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:
realtime:
driver: supabase-realtime
# [to be decided] key names; provider keys go in as secret: referencesBackups follow the db driver, so point-in-time recovery comes from the Supabase project:
backup:
rpo: 15m
rto: 1h
keep: 35d
drill: { every: 30d, to: scratch }Use it
Guard a route with Supabase Auth behind auth:
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:
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:
"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:
import { runDrill } from "mainsdk/backup";
const result = await runDrill({ driver, declared, app: "my-app", at: new Date(), to: "scratch" }); // restores into scratch, never productionmainsdk drill restore --to scratch runs the same drill from the command line and signs the evidence.
Local development and testing
- No Supabase project:
mainsdk devrunsdbon local SQLite andauthon thelocaldriver, 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/testingrunsauth,dbandrealtimeon 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 scratchon a schedule in CI.
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 checkwithMAIN-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
dbspec's driver ticket covers Postgres, Neon and SQLite; the Supabasedbdriver is listed out of scope there and comes later. - The
authspec listssupabaseamong its drivers but has no build ticket for it yet. - Supabase Realtime for
realtimehas 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
dbquery. auth.raw()anddb.raw()return the Supabase clients for features MainSDK does not wrap.mainsdk checklists every use.
Swap to or from it
Each interface swaps on its own line:
db:
driver: neon # was supabaseFAQ
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?