What is Supabase?

And why should you use it?

Erfi Anugrah

Agenda

  1. Platform overview — what it is
  2. Why Supabase — three differentiators
  3. Use cases from the field
  4. What I built with it

The Platform

Open-source Firebase alternative built on Postgres

Product What it does
Database Fully managed Postgres + Row Level Security
Auth Email, OAuth, magic link, SSO/SAML
Storage S3-compatible object store + CDN
Realtime WebSocket subscriptions (presence, broadcast, CDC)
Edge Functions Deno runtime, globally distributed

One platform. One bill. Tightly integrated.

One Platform vs. Stitching

Stitched Supabase
Database RDS / Neon + config Managed Postgres
Auth Auth0 / Clerk Built-in
Authorization App middleware RLS in the DB
Storage S3 + CloudFront Built-in + CDN
Realtime Pusher / Ably Built-in

Fewer vendors → less integration work → faster to ship

Three Differentiators

Open Source

  • 97K+ GitHub stars
  • Self-hostable on your infra
  • Building in public

Postgres-Native

  • Standard SQL, not a proprietary query language
  • Full extension ecosystem
  • pgvector, pg_cron, PostGIS

No Lock-in

  • pg_dump anytime
  • Standard connection strings
  • Your data, your keys
  • Self-host or migrate whenever

Security at the Database Layer

Row Level Security — authorization enforced by Postgres, not your app code

CREATE POLICY "own posts" ON posts FOR ALL TO authenticated
USING     (user_id = auth.uid())   -- who can SEE / touch this row
WITH CHECK(user_id = auth.uid());  -- what can be WRITTEN into this row
Clause Applies to Guards against
USING SELECT, UPDATE, DELETE Reading or deleting someone else’s rows
WITH CHECK INSERT, UPDATE Writing a row with the wrong owner — or reassigning ownership to another user

Without USING: you can update rows you don’t own. Without WITH CHECK: you can update your own row and change user_id to someone else’s.

Both together = you can only touch rows you own, and you can’t transfer ownership.

What Customers Are Building

Pattern Example Why Supabase
Startup greenfield Chatbase ($1M rev in 5 months) Ship in days, not weeks
Firebase migration Good Tape (60% cost cut) Standard SQL + no per-read billing
Multi-tenant SaaS RLS per-tenant policies Authorization at DB layer, not app layer
AI / vector pgvector for embeddings One less vendor — same DB
EU compliance GDPR + data residency 6 EU regions, SOC 2, custom DPA

Pasteriser — paste.erfi.io

A production pastebin — every Supabase surface used deliberately.

Feature What I used
Data + search Postgres, 14 migrations, FTS via tsvector + GIN index
Access control 7 RLS policies — public read, owner write, burn-after-read
Auth Email, magic link, GitHub OAuth with PKCE in a CF Worker
Live feed Realtime broadcast from a Postgres trigger
Scheduled cleanup pg_cron — replaces Lambda + EventBridge entirely
Config as code supabase/config.toml + config push

282 tests — including race-condition tests under 100 concurrent hits.

Summary

What

  • Postgres + Auth + Storage
  • Realtime + Edge Functions
  • One platform, one bill

Why

  • Open source, self-hostable
  • Postgres-native, no lock-in
  • Security at the DB layer

Who

  • Startups shipping fast
  • Firebase/Auth0 migrations
  • AI teams adding vector search

Questions?

Reference

How Full-Text Search Works

Search that understands language — not just exact word matching.

Step What happens
1. Write a paste Postgres converts title + language to root words: "running tests"run test
2. Store the index The root words are stored in a tsvector column — automatically kept in sync
3. Build a GIN index A lookup table maps every root word to which rows contain it
4. Search User types "run" — Postgres checks the index, returns matching rows in milliseconds

No Elasticsearch. No separate search service. Built into Postgres.

Realtime: Database → Browser

A browser receives an update the moment a row is written — no polling.

User saves a paste
    ↓
Postgres commits the row
    ↓
AFTER INSERT trigger fires
    ↓
realtime.send() → Realtime server → all subscribed browsers

Two functions:

  • realtime.send() — you control the payload (Pasteriser: strips private columns)
  • realtime.broadcast_changes() — sends the full row change (acme-corp notes feed)

pg_cron: Scheduled Tasks in Postgres

Run SQL on a schedule, from inside the database. No extra services.

-- Delete expired pastes every 5 minutes
SELECT cron.schedule(
  'cleanup-expired',
  '*/5 * * * *',
  $$ DELETE FROM pastes WHERE expires_at < now() $$
);
AWS Lambda + EventBridge pg_cron
Where it runs Separate serverless service Inside Postgres
Deployment Deploy function + create rule One SQL statement
Logs CloudWatch cron.job_run_details table
Cost Per invocation + rule Included with Postgres

Firebase → Supabase: Product Map

Firebase product What it does Supabase equivalent
Firestore NoSQL document database — nested collections/documents, no SQL Postgres — relational tables, standard SQL. Biggest conceptual shift.
Firebase Auth Email, Google/Apple login, phone OTP Supabase Auth — near 1:1 feature parity
Firebase Storage File uploads (photos, videos, docs) Supabase Storage — S3-compatible, RLS policies on files
Cloud Functions (HTTP) Serverless code triggered by HTTP calls Edge Functions — Deno runtime, same idea
Cloud Functions (events) Code triggered by DB changes, new users, etc. Database triggers — SQL functions that fire on row changes
Cloud Functions (scheduled) Code on a cron schedule pg_cron — SQL on a schedule, inside Postgres
FCM Push notifications to phones/browsers Edge Function + DB webhook → calls FCM or Expo — FCM still delivers, Supabase is the trigger

Multi-Tenant: The Schema

One database, many companies — each seeing only their own data.

-- Every company is a tenant
CREATE TABLE tenants (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL
);

-- Every user belongs to one tenant
CREATE TABLE profiles (
  id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  tenant_id uuid REFERENCES tenants(id)
);

-- Business data carries tenant_id on every row
ALTER TABLE notes ADD COLUMN tenant_id uuid REFERENCES tenants(id);

When a new user signs up, a database trigger automatically creates their tenant and profile. No application code needed.

Multi-Tenant: Pattern 1

Membership table lookup — look up the user’s tenant at query time

CREATE POLICY "view own tenant's notes" ON notes
  FOR SELECT
  USING (
    tenant_id IN (
      SELECT tenant_id FROM profiles WHERE id = (SELECT auth.uid())
    )
  );
  • Postgres auto-filters — wrong rows can’t be returned even if app code has a bug
  • User can belong to multiple tenants
  • Best practice: write (SELECT auth.uid()) not auth.uid() — caches the ID once per query instead of re-evaluating per row

Multi-Tenant: Pattern 2

Claim in the login tokentenant_id baked into the JWT at login

CREATE POLICY "view own tenant's notes" ON notes
  FOR SELECT
  USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid);
  • No extra database lookup — reads straight from the login token
  • Requires an Access Token Hook: a Postgres function that injects tenant_id into the token at login
  • One tenant per session — user re-logins to switch organisations
Pattern 1 Pattern 2
Setup Simple, no hooks Needs an Access Token Hook
Speed Subquery per query Single equality check
Multi-org Yes One org per session

Auth: Tier & Feature Matrix

Feature Free Pro Team Enterprise
MAU included 50K 100K 100K Custom
Custom Access Token Hook
Send SMS / Email Hook
SAML SSO
SSO MAUs included 50 50 Custom
MFA Verification Hook
Dashboard SSO
SOC 2 Type 2
ISO 27001 certificate
Uptime SLA (99.9%)
24/7 Sev-1 support
BYO Cloud

Auth: SAML SSO — Multi-Tenant Pattern

Each enterprise customer gets their own SAML connection. sso_provider_id scopes the JWT.

// Route user to their org's IdP by domain
const { data } = await supabase.auth.signInWithSSO({
  domain: 'customer-corp.com',
})
-- RLS policy using sso_provider_id as the tenant identifier
CREATE POLICY "tenant isolation"
  ON records AS RESTRICTIVE
  USING (
    provider_id = (auth.jwt() -> 'amr' -> 0 ->> 'provider')
  );

Auth: Self-Hosting Reality

Managed Cloud Self-Hosted
Officially supported deployment Docker Compose only
Kubernetes / Helm Community (supabase-community/supabase-kubernetes)
Support Enterprise SLA / Team email Community only (GitHub Discussions, Discord)
Uptime SLA ✅ Enterprise ❌ None
Branching, PITR, metrics, ETL ❌ Unavailable
Data in your own infra ❌ AWS regions
BYO Cloud (Supabase manages in your cloud) Enterprise (negotiated)

Competitive: Auth Vendors — Azure-Native Stacks

Supabase Auth0 Entra External ID
Cost at <50K MAU From $25/mo (Pro) From ~$1,400/mo $0 (50K MAU free tier)
ANZ data residency ✅ Sydney, all tiers Enterprise only (AWS AU) ✅ AU Go-Local add-on (paid)
ISO 27001 ✅ Team/Enterprise
SAML SSO Pro+ Enterprise ✅ native
.NET integration Standard JWKS / AddJwtBearer SDK ✅ Microsoft.Identity.Web
Open source / self-hostable
Platform beyond auth ✅ Postgres + Storage + Realtime

Reference: Admin Impersonation Pattern

sequenceDiagram
    actor A as Admin
    participant EF as Edge Function
    participant API as Backend APIs

    A->>EF: POST /impersonate { target_user_id }
    Note over EF: verify admin JWT + is_admin claim
    Note over EF: fetch target user via service role,<br/>mint JWT — sub: target · act: admin · exp: +1h
    EF-->>A: impersonation token (short-lived, no refresh)
    A->>API: request + Bearer impersonation token
    Note over API: validates as target user · act claim for audit

Whiteboard Scenarios

Scenario A: Firebase Migration

Most teams migrate incrementally — Auth first (users are portable), then data, then functions.

The biggest conceptual shift is Firestore → Postgres: document collections become relational tables. Everything else maps 1:1.

Scenario A: Product Map

Firebase Supabase
Firestore (NoSQL docs) biggest shift → Postgres (SQL tables)
Firebase Auth 1:1 → Supabase Auth
Firebase Storage 1:1 → Supabase Storage
Cloud Functions (HTTP) Edge Functions
Cloud Functions (events) Database Triggers
Cloud Functions (scheduled) pg_cron
FCM (push notifications) more setup → Edge Function + DB webhook calls FCM/Expo

Scenario B: Auth + Existing IdP

SAML assertion in — enriched JWT out. The IdP authenticates; Supabase issues the token.

The Access Token Hook runs before the JWT is signed, injecting custom claims (tenant_id, role, plan) without a round-trip to the application.

Scenario B: JWT Flow

sequenceDiagram
    actor U as User
    participant IdP as Company IdP
    participant Auth as Supabase Auth
    participant Hook as Access Token Hook
    participant DB as Postgres + RLS

    U->>IdP: Sign in with company account
    IdP-->>Auth: SAML assertion
    Auth->>Hook: Raw JWT payload
    Hook-->>Auth: + tenant_id, role
    Auth-->>U: Signed JWT
    U->>DB: API request + JWT
    DB->>DB: RLS policy filters rows
    DB-->>U: Filtered rows only

Scenario B: BYO Backend

Supabase Auth as a standalone identity layer — any backend validates the JWT via a standard JWKS endpoint.

No Supabase SDK required server-side. RS256-signed JWT; standard library support in .NET, Go, Java, Python, Node.

Scenario B: BYO Backend JWT Flow

sequenceDiagram
    actor U as User
    participant Auth as Supabase Auth
    participant Hook as Access Token Hook
    participant API as Customer API (any backend)
    participant DB as Customer DB (SQL Server / DB2 / etc.)

    U->>Auth: Sign in (email / SAML)
    Auth->>Hook: Raw JWT payload
    Hook-->>Auth: + tenant_id, role
    Auth-->>U: Signed JWT
    U->>API: Request + Bearer JWT
    API->>API: Validate via JWKS endpoint
    API->>DB: Query (tenant-scoped)
    DB-->>API: Rows
    API-->>U: Response

Scenario C: Multi-Tenant SaaS

Row Level Security enforces tenant isolation at the database layer — authorization that can’t be bypassed by application code.

Two patterns: JWT claim (fast, single-tenant-per-session) or membership table (flexible, multi-org users).

Scenario C: Schema

erDiagram
    auth_users {
        uuid id PK
        text email
    }
    tenants {
        uuid id PK
        text name
    }
    profiles {
        uuid id PK
        uuid tenant_id FK
        text full_name
    }
    notes {
        uuid id PK
        uuid user_id FK
        uuid tenant_id FK
        text content
    }

    auth_users ||--|| profiles : "trigger on signup"
    tenants ||--o{ profiles : "belongs to"
    tenants ||--o{ notes : "belongs to"
    auth_users ||--o{ notes : "creates"