Technical Discovery
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)
What we understand:
What we’d love to learn:
Your current state
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 |
Compliance
Operations
Team
Timeline
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
Row Level Security — authorization at database level
Request → JWT validated → RLS policy checked → filtered rows
| 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.
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
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.
| Plan | Uptime | Urgent Response | DPA |
|---|---|---|---|
| Pro | None | No | |
| Team | None | 24h 24/7 | Yes |
| Enterprise | 99.9% | 1h 24/7 | Yes (Custom) |
99.9% = ~43 min downtime/month — Credits 10-30% of monthly fees, capped at 20% of preceding 12 months
| Type | RPO | Use case |
|---|---|---|
| Daily backups | 24h | Standard |
| PITR | ~2 min | Precise recovery |
PITR: Restore to any second. $100/mo for 7 days.
EU Users → Supabase API (geo-routing)
│
┌───────────┼───────────┐
▼ ▼ ▼
Primary Replica Replica
(Region TBD)
Region selection depends on where your users are concentrated.
| Foundation | Build | Launch | |
|---|---|---|---|
| Effort | ████ | ████████████████ | ████ |
| Project setup | Data APIs + RLS | Load testing | |
| Auth | Storage | Security review | |
| CI/CD | Realtime | Monitoring | |
| Integrations | Go-live |
I’ll send:
Follow-ups:
| Feature | Pro | Team | Enterprise |
|---|---|---|---|
| SLA | None | None | 99.9% |
| Urgent support | 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 |
| 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.
| Resource | URL |
|---|---|
| Docs | supabase.com/docs |
| Pricing | supabase.com/pricing |
| Status | status.supabase.com |
| Security | supabase.com/security |
┌─────────────┐ HTTPS + JWT ┌─────────────┐
│ supabase-js │ ───────────────────▶ │ PostgREST │
│ (Browser) │ ◀─────────────────── │ (API Layer) │
└─────────────┘ JSON response └──────┬──────┘
│
SQL + JWT context
▼
┌─────────────┐
│ Postgres │
│ + RLS │
└─────────────┘
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 |
| Clause | Controls |
|---|---|
USING |
Which rows accessible |
WITH CHECK |
Which values valid |
JWT-based (fast):
Membership table (flexible):
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
| Method | Validates? |
|---|---|
getSession() |
No (trusts cookie) |
getUser() |
Yes (calls Auth) |
Rule: Use getUser() server-side for trusted identity
bucket: user-files
├── {user_id}/avatar.jpg
└── {user_id}/docs/resume.pdf
| 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)
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).
| 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 |
| 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.
// 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 */ }
}
}
});
}| 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)
GDPR Art. 17: users can request deletion of all their data.
Delete via Dashboard (Auth > Users) or Admin API (auth.admin.deleteUser()).
Storage objects must be deleted first (not cascaded, and blocks user deletion).
Supabase + Acme | Technical Discovery