Supabase + Acme

Technical Discovery

Erfi Anugrah

Agenda

  1. Intros
  2. Discovery — your EU expansion goals
  3. How Supabase addresses your needs
  4. Proposed approach
  5. Q&A + next steps

Supabase in 30 Seconds

Open-source Firebase alternative on Postgres

Feature What
Database Postgres + Row Level Security
Auth OAuth, email, SSO/SAML
Storage S3-compatible + CDN
Realtime WebSocket subscriptions
Functions Deno edge runtime

Why: Ship fast, security at DB layer, no infra overhead (unless self-hosting)

Discovery

Your EU Expansion

What we understand:

  • Greenfield Next.js app for European market
  • ~5-6M MAUs in US, expecting 2.5-3M in EU
  • Target go-live: 3-6 months

What we’d love to learn:

  • What’s driving the timeline?
  • What does success look like in 6 months?

Technical Landscape

Your current state

  • Any specific pain points you want to avoid?
  • What does your current architecture look like?

Feature Requirements

What features are you planning to use?

Auth Realtime Storage Functions
Email/password Notifications User uploads Custom logic
Social logins Live feeds Documents Integrations
SSO/SAML Collaboration Media files Webhooks
Existing IdP Presence CDN delivery Cron jobs

Security & Compliance

Compliance

  • GDPR Requirements
  • Does your security team need a SOC 2 report?
  • DPA required?

Operations

  • What uptime do you need?
  • Disaster recovery expectations?

Team & Timeline

Team

  • Engineering team size
  • Postgres experience level
  • DevOps/Platform support

Timeline

  • Hard deadline vs target
  • MVP scope definition
  • Phased rollout vs big bang

What I’m Hearing

  • Your top priority is [X]
  • Your biggest concern is [Y]
  • Key constraint: [Z]

How Supabase Helps

Speed to Market

One platform vs stitching services

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

→ Fewer vendors, one bill, tighter integration

Security Model

Row Level Security — authorization at database level

Request → JWT validated → RLS policy checked → filtered rows
  • Every query filtered before execution
  • Can’t bypass via API bugs
  • No backend auth code needed

→ Deep-dive: RLS

EU Compliance

Requirement Solution
Data residency 6 EU regions — fixed at project creation
SOC 2 Type 2 Annual audit, report available
DPA Team/Enterprise plans
Right to erasure Cascade delete from auth.users

→ EU regions | → Right to erasure

Warning

Region is permanent. Cross-region migration = new project + manual dump/restore. Choose based on where your users are concentrated.

Scaling

Your scale: 5-6M US + 2.5-3M EU MAUs

Challenge Solution
Connection limits Supavisor pooling
Read traffic Read replicas + geo-routing (load balancer auto-routes)
Compute Scale to 64-core

Vercel: Use transaction mode pooling

→ Pooling | → Compute tiers

Note

Replica caveat: replicas serve GET requests only. RPCs need get: true and must be truly read-only. Any mutation (e.g. a view counter inside a function) always hits the primary.

SLAs

Plan Uptime Urgent Response DPA
Pro None Email No
Team None 24h 24/7 Yes
Enterprise 99.9% 1h 24/7 Yes (Custom)

99.9% = ~43 min downtime/monthCredits 10-30% of monthly fees, capped at 20% of preceding 12 months

Disaster Recovery

Type RPO Use case
Daily backups 24h Standard
PITR ~2 min Precise recovery

PITR: Restore to any second. $100/mo for 7 days.

Proposed Approach

Architecture (Example)

EU Users → Supabase API (geo-routing)
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
    Primary      Replica     Replica
   (Region TBD)

Region selection depends on where your users are concentrated.

Typical Timeline

Foundation Build Launch
Effort ████ ████████████████ ████
Project setup Data APIs + RLS Load testing
Auth Storage Security review
CI/CD Realtime Monitoring
Integrations Go-live

Next Steps

I’ll send:

  • Summary email with recommendations
  • EU compliance docs
  • SOC 2 report (NDA)

Follow-ups:

Questions?

Appendix

Plans

Feature Pro Team Enterprise
SLA None None 99.9%
Urgent support Email 24h 24/7 1h 24/7
SOC 2 No Yes Yes
DPA No Yes Yes (Custom)
SSO (your app) Yes Yes Yes
Dashboard SSO No Yes Yes
Backups 7 days 14 days 30 days

← Back

EU Regions

Code Location Role
eu-central-1 Frankfurt Primary
eu-west-2 London Replica
eu-north-1 Stockholm Replica
eu-central-2 Zurich Available
eu-west-1 Dublin Available
eu-west-3 Paris Available

Region locked at creation — cross-region = new project + dump/restore. Pick by RTT, not just geography.

← Back

Links

Resource URL
Docs supabase.com/docs
Pricing supabase.com/pricing
Status status.supabase.com
Security supabase.com/security

Technical Deep-Dive

Request Flow

┌─────────────┐     HTTPS + JWT      ┌─────────────┐
│ supabase-js │ ───────────────────▶ │  PostgREST  │
│  (Browser)  │ ◀─────────────────── │ (API Layer) │
└─────────────┘     JSON response    └──────┬──────┘
                                            │
                                     SQL + JWT context
                                            ▼
                                     ┌─────────────┐
                                     │  Postgres   │
                                     │   + RLS     │
                                     └─────────────┘

← Back

auth.uid()

Postgres function reading session variable:

1. Login → JWT with 'sub' (UUID)
2. Request includes Bearer token
3. PostgREST: SET request.jwt.claims
4. auth.uid() reads claims->>'sub'
Function Returns
auth.uid() User UUID (jwt->>'sub')
auth.jwt() Full JWT payload

RLS Policies

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy
CREATE POLICY "own_posts" ON posts
FOR ALL TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
Clause Controls
USING Which rows accessible
WITH CHECK Which values valid

← Back

Multi-Tenant Patterns

JWT-based (fast):

USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)

Membership table (flexible):

USING (tenant_id IN (
  SELECT tenant_id FROM members WHERE user_id = auth.uid()
))

OAuth Flow

1. Click "Sign in with Google"
2. Redirect to Google
3. Google returns auth code
4. Supabase exchanges for tokens
5. Extract user info, store provider tokens
6. Create Supabase JWT
7. Return to app

PKCE protects against code interception

getSession vs getUser

Method Validates?
getSession() No (trusts cookie)
getUser() Yes (calls Auth)

Rule: Use getUser() server-side for trusted identity

Storage

bucket: user-files
├── {user_id}/avatar.jpg
└── {user_id}/docs/resume.pdf
CREATE POLICY "own_files" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
  (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

Realtime

Feature Source Persisted
Postgres Changes DB Yes
Broadcast Client→Client or DB trigger No
Presence Client state No

Recommended: Broadcast from Database (via realtime.broadcast_changes() trigger)

// Private channel + setAuth() required
await supabase.realtime.setAuth()
supabase.channel(`notes:${tenantId}`, { config: { private: true } })
  .on("broadcast", { event: "INSERT" },
      (payload) => console.log(payload.payload?.record))
  .subscribe();

Connection Pooling

Problem: Postgres has limited connections. Serverless opens many.

Mode Host Port Use Note
Transaction (Supavisor) aws-N-<region>.pooler.supabase.com 6543 Vercel/serverless Release per txn; no prepared stmts
Session (Supavisor) aws-N-<region>.pooler.supabase.com 5432 Long-lived services 1:1, full Postgres parity
Direct db.<ref>.supabase.co 5432 Migrations, DDL IPv6 raw; required for long DDL
PgBouncer (paid) db.<ref>.supabase.co 6543 High-perf dedicated Co-located; txn mode only

ORM? Disable prepared statements for txn mode — ?pgbouncer=true (Prisma), statement_cache_size=0 (asyncpg).

← Back

Compute Tiers

Size CPU RAM Connections ~Monthly
Micro 2 shared 1GB 60 ~$10
Small 2 shared 2GB 90 ~$15
Medium 2 shared 4GB 120 ~$60
Large 2 dedicated 8GB 160 ~$110
XL 4 dedicated 16GB 240 ~$210
2XL 8 dedicated 32GB 380 ~$410
4XL 16 dedicated 64GB 480 ~$960
8XL 32 dedicated 128GB 490 ~$1,870
12XL 48 dedicated 192GB 500 ~$2,800
16XL 64 dedicated 256GB 500 ~$3,730

← Back

Monitoring

Metric Action threshold
CPU >70% sustained
Memory >80% sustained
Connections >80% of limit
Replication lag >10s
p95/p99 latency Degrading
Missing indexes Studio → Advisors → Query Performance

index_advisor — built on hypopg (hypothetical indexes, zero build cost). Run index_advisor(query) or use the Studio Query Performance → Indexes tab to get recommended CREATE INDEX statements with planner cost before/after, without building anything first.

SSR (Next.js)

// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(URL, KEY, {
    cookies: {
      getAll: () => cookieStore.getAll(),
      setAll: (c) => {
        try {
          c.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options));
        } catch { /* Server Component - read-only */ }
      }
    }
  });
}

Vercel + Supabase

Integration What it does
Vercel Integration Auto-injects env vars into Vercel project
@supabase/ssr Cookie-based auth for SSR/RSC
Transaction mode pooling Port 6543 for serverless (Supavisor)
Database Branching Preview branch per Vercel preview deployment

Key: Serverless = many short connections → always use transaction mode (port 6543)

Right to Erasure

GDPR Art. 17: users can request deletion of all their data.

-- Set up CASCADE on foreign keys (from docs)
create table public.profiles (
  id uuid not null references auth.users on delete cascade,
  first_name text,
  last_name text,
  primary key (id)
);

Delete via Dashboard (Auth > Users) or Admin API (auth.admin.deleteUser()).

Storage objects must be deleted first (not cascaded, and blocks user deletion).

← Back

CLI

supabase start          # Local stack
supabase db reset       # Reset + migrations
supabase db push        # Push to prod
supabase gen types typescript --local