0004 — Supabase as the managed backend
Status: Accepted Date: 2026-04 Deciders: TwoMore mobile team
Context
TwoMore needs: relational data storage (clubs, sessions, matches, rankings, dues), row-level security so club members only see their club's data, low-latency freshness for live scorecard surfaces, auth (Kakao + Apple + Google), and server-side logic for trust-tier computation, attendance triggers, and ELO recalculation.
The team is two engineers building an agentic-first codebase. There are no dedicated backend engineers. Server-side code must be shippable without provisioning or managing infrastructure.
Key data shape drivers:
- Leaderboards and ranking queries are relational and require JOINs, window functions, and sorted aggregates — NoSQL is a poor fit
- RLS policies must be the security perimeter, not application-level guards that can be bypassed by a client bug
- Live scorecard updates require bounded, focus-aware freshness. As of 2026-06-27, the implementation uses focused polling rather than persistent Postgres-Changes sockets.
- 303 migrations as of 2026-06-27 — schema evolution is a first-class operation, not an afterthought
Options considered
Option A: Firebase (Firestore + Cloud Functions)
- Pros: mature ecosystem; solid realtime; easy auth with Google
- Cons: NoSQL document model fights ELO ranking queries (sorted aggregates, window functions, cross-collection JOINs are not natural); Firestore security rules are less expressive than SQL RLS; no
generate typestoolchain for the client - Effort: high — ranking and leaderboard features would need denormalised structures maintained by Cloud Functions
- Reversibility: low
Option B: Custom Express + PostgreSQL on a VPS/Railway/Render
- Pros: full control; no vendor lock-in
- Cons: we would be writing auth, realtime, connection pooling, migrations, and RLS-equivalent logic from scratch; requires at least one backend engineer to maintain; operational burden (TLS, backups, monitoring) falls entirely on us; agentic contributors would need to context-switch between RN code and server code constantly
- Effort: very high — 2-3 weeks to reach feature parity with Supabase's built-ins
- Reversibility: high (we own everything)
Option C: AWS Amplify Gen 2
- Pros: strong toolchain; integrates with CDK for complex infrastructure
- Cons: more configuration overhead than Supabase; the GraphQL API layer adds a translation step over what is ultimately a relational store; auth story for Kakao requires a custom Lambda; steeper learning curve for agentic contributors
- Effort: high
- Reversibility: medium
Option D: Supabase (managed PostgreSQL + RLS + realtime + auth + edge functions)
- Pros: PostgreSQL with full SQL power for ranking queries and window functions; RLS as the trust boundary (server-enforced, not bypassable from the client); optional realtime infrastructure if a future freshness-tier decision justifies it; Supabase Auth handles Kakao/Apple/Google via PKCE; Edge Functions (Deno) for server-side logic without managing servers;
npx supabase gen typesproducesgenerated.types.tsthat the TypeScript adapter consumes; migration toolchain (npx supabase db push) is first-class - Cons: vendor lock-in to Supabase's hosted Postgres and realtime infrastructure; RLS policy bugs silently return empty result sets rather than errors — they're hard to debug; pricing scales with realtime connections and database egress; Supabase Auth's Kakao provider requires a custom Edge Function to verify the Kakao token before issuing a Supabase JWT
- Effort: ~1 week setup + incremental migration authoring
- Reversibility: low — 151 migrations and RLS policies are tightly coupled to Supabase's
auth.uid()function; migrating to a different Postgres host would require rewriting all policies
Decision
Option D — Supabase as the managed backend.
Primary reason: the relational data model, RLS as a hard security boundary, and server-side trusted workflows all point to a managed PostgreSQL host with strong auth, storage, functions, and migration tooling. Supabase provides those without requiring a dedicated backend engineer or significant infrastructure management overhead.
Dissenting consideration: vendor lock-in is real and the pricing model could become painful at scale. Mitigated by: Supabase is backed by standard PostgreSQL; the lock-in is primarily in RLS policy syntax (auth.uid()) and the realtime subscription protocol, not in query logic — a migration to a self-hosted Postgres + custom realtime layer is painful but not impossible.
Consequences
Positive
- RLS policies are the security perimeter — every table has policies, and ARCH-3 ensures
generated.types.tsstays inside the adapter layer so no presentation code can construct raw SQL against the wrong schema - 303 migration files in
supabase/migrations/give an auditable schema history;npx supabase db resetreproduces the full schema from scratch - Persistent realtime channels are currently retired. Live freshness is handled through TanStack Query, focused polling, push-triggered cache busts, and mutation invalidation. Realtime guardrails remain for any future reintroduction.
- Edge Functions (Deno) handle trust-tier recomputation, ELO confidence bonuses, attendance triggers, and Alimtalk dispatch — no server provisioning required
Negative
- RLS policy bugs return empty result sets, not errors — they can silently hide data from users and are difficult to distinguish from "the query returned nothing" in the client
- Every new table requires:
CREATE TABLE+ENABLE ROW LEVEL SECURITY+ policies + trigger protection in the same migration — omitting any step is a security gap - Supabase Auth's Kakao provider requires a custom Edge Function to verify the Kakao
id_tokenbefore issuing a Supabase JWT (seeauthentication.md) - Type drift:
generated.types.tsgoes stale when the DB schema changes andnpx supabase gen typesisn't re-run — mitigated by theyarn check:supabase-typesgate (opt-in viaSUPABASE_ACCESS_TOKEN)
Neutral
- The RLS trust model means the client always authenticates as
authenticatedrole; theservice_rolekey never leaves Edge Functions
Implementation notes
- Adapter:
packages/app/src/adapters/supabase/ - Generated types:
packages/app/src/adapters/supabase/generated.types.ts(ARCH-3 confined) - Realtime: no persistent client socket registry is active as of 2026-06-27; see CLAUDE.md freshness tiers
- Auth adapter:
packages/app/src/adapters/supabase/auth.adapter.ts - Migration directory:
supabase/migrations/(303 files as of 2026-06-27) - Edge Functions:
supabase/functions/
References
- api-supabase.md
- authentication.md
- security.md
- AGENTS.md — ARCH-3 (generated types confinement), DATA-5 (auth adapter boundary)
- stack-decisions.md — original auth and payment stack research