How it works

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.

@supabase/supabase-js
with Supabase
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(URL, ANON_KEY);

const { data, error } = await supabase
  .from("posts")
  .select("*")
  .eq("published", true);
fakebase
with Fakebase
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

Your app
Next.js · SvelteKit · Astro · Remix · Node — any framework
createClient(url, key, { kernel })
Supabase-shaped facade — from() · auth · storage · realtime · rpc
FakebaseKernel
schema IR · query compiler · policy engine · capability registry
Adapter
memory · json · sqlite · pglite — interchangeable persistence

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 }.

  1. 1Your call
    your app

    The exact @supabase/supabase-js chain — nothing Fakebase-specific.

    supabase
      .from("posts")
      .select("*")
      .eq("published", true)
  2. 2Facade
    @byronwade/client

    The builder records the chain; nothing executes until you await it.

    DatabaseBuilder {
      table: "posts",
      filters: [{ column: "published", op: "eq", value: true }],
    }
  3. 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: "*",
    }
  4. 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
  5. 5Adapter
    adapter-memory

    The plan runs against the store; every adapter passes the same contract suite.

    store.get("public.posts")
      .filter(r => r.published === true)
  6. 6Result
    back 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.

01

API-shape fidelity

Method names, chaining, and { data, error } envelopes match @supabase/supabase-js exactly.

Promised
02

Behavior fidelity

Insert/select/update/filter, sessions, and storage behave like Supabase for the common cases.

Promised for supported capabilities
03

Infrastructure fidelity

True Postgres planner semantics, real RLS enforcement, edge routing, PITR. This is why Fakebase is dev-only.

Not promised

What 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.

AdapterSetupDurabilityBest for
adapter-memoryNoneProcess onlyTests, disposable prototypes
adapter-jsonNone.fakebase/ filesSmall prototypes, seeds, manual editing
adapter-sqliteLow (native build)Single-file (WAL)Serious local projects
adapter-pgliteNone (pure WASM)Directory or in-memoryHighest SQL fidelity, CI
Export & ship

Fakebase is designed to be thrown away

  1. 1
    fakebase migrate export --supabase

    Write supabase/migrations/*.sql from your schema IR.

  2. 2
    fakebase seed export

    Generate supabase/seed.sql from local data.

  3. 3
    fakebase types gen

    Emit database.types.ts — identical shape to Supabase's generator.

  4. 4
    fakebase verify supabase

    Run the compatibility suite against a real Supabase stack.

  5. 5
    swap createClient

    Replace fakebase's createClient with @supabase/supabase-js. Done.