---
title: "Supabase"
description: "Use Supabase for MainSDK auth, data, real-time updates and backups - project setup, main.yaml per interface, secrets, local testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/supabase"
updated: "2026-10-28"
---

# Supabase

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

Canonical: https://mainsdk.dev/docs/integrations/supabase · 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`](https://mainsdk.dev/docs/interfaces/auth) through Supabase Auth, [`db`](https://mainsdk.dev/docs/interfaces/db) on the project's Postgres database, [`realtime`](https://mainsdk.dev/docs/interfaces/realtime) through Supabase Realtime, and [`backup`](https://mainsdk.dev/docs/interfaces/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

### Sign-in and sessions (`auth`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `supabase` (Supabase Auth)
- Docs: https://mainsdk.dev/docs/interfaces/auth.md

```yaml
auth:
  driver: supabase
```

Swap to another provider: `github`, `clerk`, `auth0`, `better-auth`, `workos`, `local`.

### Data and records (`db`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `supabase` (Supabase)
- Docs: https://mainsdk.dev/docs/interfaces/db.md

```yaml
db:
  driver: supabase
```

Swap to another provider: `postgres`, `neon`, `sqlite`.

### Realtime updates (`realtime`)

- Status: Building for 0.3 · ships as a beta API
- Driver: `supabase-realtime` (Supabase Realtime). later
- Docs: https://mainsdk.dev/docs/interfaces/realtime.md

```yaml
realtime:
  driver: supabase-realtime
```

Swap to another provider: `broadcast`, `local`, `pusher`, `ably`.

### Backups and restore drills (`backup`)

- Status: Building for 1.2 · ships as a beta API
- Driver: `supabase-pitr` (Supabase point-in-time recovery)
- Docs: https://mainsdk.dev/docs/interfaces/backup.md

```yaml
backup:
  driver: supabase-pitr
```

Swap to another provider: `local`, `neon-pitr`, `pgbackrest`, `aws-backup`, `object-lock`.

## 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:

```yaml title="main.yaml"
auth:
  driver: supabase
  methods: [passkeys]
  # [TBD] key names; suggested secrets: SUPABASE_URL, SUPABASE_SECRET_KEY
```

Data, one Supabase project per region:

```yaml title="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:

```yaml title="main.yaml"
realtime:
  driver: supabase-realtime
  # [TBD] key names; provider keys go in as secret: references
```

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

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

> **Note:** `realtime` has no spec yet; its options here are provisional and may change when the spec lands.

## Use it

Guard a route with Supabase Auth behind `auth`:

```ts title="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:

```ts title="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:

```ts title="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:

```ts title="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.

```ts title="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](https://mainsdk.dev/docs/interfaces/auth), [Data and records](https://mainsdk.dev/docs/interfaces/db), [Real-time updates](https://mainsdk.dev/docs/interfaces/realtime) and [Backups and restore drills](https://mainsdk.dev/docs/interfaces/backup).

## 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:

```yaml title="main.yaml"
db:
  driver: neon   # was supabase
```

> **Warning:** Supabase holds state for three of these interfaces. Users live in Supabase Auth, rows live in the project's database, and backups stay with the project you leave. Moving users or rows is a migration that MainSDK runs as a guided job with a rollback, and you should run a restore drill on the new provider before you rely on it. `realtime` holds no state and swaps with no migration.

## 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

- [Sign-in and sessions](https://mainsdk.dev/docs/interfaces/auth), [Data and records](https://mainsdk.dev/docs/interfaces/db), [Real-time updates](https://mainsdk.dev/docs/interfaces/realtime) and [Backups and restore drills](https://mainsdk.dev/docs/interfaces/backup)
- [Postgres](https://mainsdk.dev/docs/integrations/postgres), [Neon](https://mainsdk.dev/docs/integrations/neon) and [Pusher](https://mainsdk.dev/docs/integrations/pusher)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
