Skip to content

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 types toolchain 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 types produces generated.types.ts that 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.ts stays 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 reset reproduces 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_token before issuing a Supabase JWT (see authentication.md)
  • Type drift: generated.types.ts goes stale when the DB schema changes and npx supabase gen types isn't re-run — mitigated by the yarn check:supabase-types gate (opt-in via SUPABASE_ACCESS_TOKEN)

Neutral

  • The RLS trust model means the client always authenticates as authenticated role; the service_role key 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

Markdown remains the source of truth. Run yarn docs:check before handoff.