The idiomatic Supabase variant
geenius-mvp ships two backends, and the tempting way to build the second would have been a shim: keep the Convex-shaped code and emulate it on Postgres. We did the opposite. --db supabase is a full idiomatic variant — its own strong stack that plays to what Supabase is actually good at. If you picked Supabase because you want SQL, you should get SQL.
schema.sql owns the logic
In the Convex variant, the backend is TypeScript modules. In the Supabase variant, the backend is supabase/schema.sql. Tables, policies, triggers, and functions live there, and you run it against your project before the app starts. Business rules that belong in the database are in the database: triggers provision user profiles and write audit and notification rows; RPCs handle the operations that must be atomic and privileged, like team creation, invite acceptance, and role changes.
RLS is the API surface
There is no per-endpoint handler layer for ordinary reads and writes. The client queries tables directly through supabase-js, and row-level security policies decide what each user can see and touch. That inverts where authorization lives — instead of remembering to guard every handler, you declare the policy once on the table and every access path inherits it. It is why cloning the records pattern is even cheaper here: the Convex version copies two backend modules, while the Supabase version copies one shared-constants file, because RLS already is the backend.
Edge Functions for everything that needs a server
Secrets and privileged operations go to Edge Functions. The ai feature calls Claude from an ai-chat function with the API key in function secrets. Billing ships stripe-checkout and a signature-verified stripe-webhook. The api-keys feature stores only hashes, verifies them with a verify_api_key service-role SQL function, and exposes an agent-api function with example REST routes. The mcp feature serves the full MCP protocol — OAuth 2.1 discovery included — from an Edge Function. Uploads use Supabase Storage with storage policies and signed URLs.
Deliberately no realtime
The data flow is request and response: a useSupaQuery hook plus explicit refetch after mutations.
const { data: rows, refetch } = useSupaQuery(
(sb) => sb.from("records").select("*").order("created_at"),
);
// after a write:
await sb.from("records").insert(payload);
refetch();
Supabase has realtime, but subscribing every list to it would be a Convex impression, not a Supabase idiom — and it would buy complexity an MVP does not need. Explicit refetch is predictable, easy to reason about, and trivial for an agent to extend. If you want reactive-by-default, that is what the Convex variant is for; the honest comparison between the two is the product.
Full parity, different shapes
As of 2026-07-19 the Supabase variant reached 100 percent feature parity: every Convex feature has a Supabase-idiomatic counterpart, down to activity mapping onto the audit_log. Same shell, same four layouts, same 100-design chrome via the Convex-free @geenius-mvp/shell/supabase entrypoint — but the backend thinks in SQL. Setup asks slightly more of you (a Supabase project, schema.sql applied, keys in .env.local), and in exchange the whole backend is one readable file plus a handful of functions.
Its own strong stack, not a Convex mimic.
Two backends, two philosophies, one scaffolder flag. Pick the one whose idiom you want to live in.