One contract. Swappable guts.
Fakebase is built around a single idea: keep the Supabase developer contract stable while swapping the implementation underneath. The compatibility logic lives in one place — @byronwade/core — and everything user-facing sits on top of it.
The same call, a swappable backend
Only the import and the createClient line differ. The query you write is byte-for-byte identical — which is why migrating to real Supabase is a one-line swap.
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(URL, ANON_KEY);
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("published", true);import { createClient, createMemoryKernel } from "@byronwade/fakebase";
const supabase = createClient("local", "dev-key", {
kernel: createMemoryKernel(schema),
});
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("published", true);The request path
Anatomy of a query
Follow one real call all the way down and back. The shapes below are representative — run the real thing live in the playground to see the actual { data, error }.
- 1Your callyour app
The exact @supabase/supabase-js chain — nothing Fakebase-specific.
supabase .from("posts") .select("*") .eq("published", true) - 2Facade@byronwade/client
The builder records the chain; nothing executes until you await it.
DatabaseBuilder { table: "posts", filters: [{ column: "published", op: "eq", value: true }], } - 3Query compiler@byronwade/core
The chain is compiled into a QueryPlan the adapter can run.
QueryPlan { schema: "public", table: "posts", operation: "select", filters: [{ column: "published", op: "eq", value: true }], orderBy: [], projection: "*", } - 4Policy engine@byronwade/core
USING / WITH CHECK predicates run for the role. (RLS off here → allow; with RLS on, a JS predicate filters rows.)
role: "anon" · rlsEnabled: false → allow all rows - 5Adapteradapter-memory
The plan runs against the store; every adapter passes the same contract suite.
store.get("public.posts") .filter(r => r.published === true) - 6Resultback to your app
The familiar { data, error } envelope — identical to Supabase.
{ data: [{ id, title, published: true, … }], error: null }
Three layers of fidelity
Fakebase promises the first two and is explicit that it does not promise the third. That honesty is the whole point.
API-shape fidelity
Method names, chaining, and { data, error } envelopes match @supabase/supabase-js exactly.
PromisedBehavior fidelity
Insert/select/update/filter, sessions, and storage behave like Supabase for the common cases.
Promised for supported capabilitiesInfrastructure fidelity
True Postgres planner semantics, real RLS enforcement, edge routing, PITR. This is why Fakebase is dev-only.
Not promisedWhat the kernel owns
FakebaseKernel is constructed synchronously, so you can use it in module scope, Server Components, and Route Handlers with no async bootstrap.
Schema IR
One normalized in-memory shape. Both the TypeScript schema DSL and SQL migrations resolve into it.
Query compiler
Translates a builder chain into a QueryPlan — filters, ordering, range, projection — then executes against the adapter.
Policy engine
Compiles USING / WITH CHECK into JS predicates over (row, context) and enforces anon / authenticated / service_role grants.
Capability registry
Every feature carries a status. Unsupported calls throw a CapabilityError with a docs link — never a silent no-op.
Pick your fidelity
Adapters implement one persistence interface and are interchangeable without touching app code. Every adapter is verified against the same behavioral contract suite, so swapping never changes observable behavior.
| Adapter | Setup | Durability | Best for |
|---|---|---|---|
adapter-memory | None | Process only | Tests, disposable prototypes |
adapter-json | None | .fakebase/ files | Small prototypes, seeds, manual editing |
adapter-sqlite | Low (native build) | Single-file (WAL) | Serious local projects |
adapter-pglite | None (pure WASM) | Directory or in-memory | Highest SQL fidelity, CI |
Fakebase is designed to be thrown away
- 1
fakebase migrate export --supabaseWrite supabase/migrations/*.sql from your schema IR.
- 2
fakebase seed exportGenerate supabase/seed.sql from local data.
- 3
fakebase types genEmit database.types.ts — identical shape to Supabase's generator.
- 4
fakebase verify supabaseRun the compatibility suite against a real Supabase stack.
- 5
swap createClientReplace fakebase's createClient with @supabase/supabase-js. Done.