Getting Started
This walkthrough takes you from an empty project to a working, data-backed app — with zero
backend setup — and then shows the short path to a real Supabase project. It works in any
framework that speaks the Supabase client contract (Next.js, SvelteKit, Astro, Remix, plain
Node, and more); the examples below use Next.js for concreteness. Every step uses the exact
@supabase/supabase-js call shape, so nothing you learn here is throwaway.
[!NOTE] Fakebase is local/dev-only. It is not production auth, authorization, or infrastructure. The goal is to get you to a working prototype fast, then out of the way.
1. Install
pnpm add @byronwade/fakebase
You need Node >=20. No Docker, no local Postgres, no migrations to run first.
2. Define your schema and client
The kernel is backed by Node built-ins (fs, crypto), so it runs only on the server.
Create a single server-only module that owns the schema and exports a client.
// lib/fakebase.ts (server-only)
import "server-only";
import { createClient, createMemoryKernel } from "@byronwade/fakebase";
import type { ProjectSchemaIR } from "@byronwade/fakebase";
import type { Database } from "@/database.types";
const schema: ProjectSchemaIR = {
version: 1,
enums: [],
functions: [],
tables: [
{
schema: "public",
name: "posts",
primaryKey: "id",
rlsEnabled: false,
policies: [],
indexes: [],
columns: [
{
name: "id",
type: "uuid",
nullable: false,
primaryKey: true,
defaultSql: "gen_random_uuid()",
},
{ name: "title", type: "text", nullable: false },
{ name: "published", type: "bool", nullable: false, defaultSql: "false" },
{
name: "created_at",
type: "timestamptz",
nullable: false,
defaultSql: "now()",
},
],
},
],
};
const kernel = createMemoryKernel<Database>(schema);
export const supabase = createClient<Database>("local", "dev-key", { kernel });
createMemoryKernel is synchronous — there is no async bootstrap — so you can construct it in
module scope and import supabase anywhere on the server.
Want durability instead of an in-memory store? Swap
createMemoryKernelforcreateJsonKernel(writes to.fakebase/) orcreateSqliteKernel. The app code does not change. See Architecture.
Optional: fill it with fake data
An empty database is no fun to build against. @byronwade/seed generates realistic,
referentially-correct rows straight from your schema — emails look like emails, foreign
keys point at real parent rows, enums stay valid:
import { seedClient } from "@byronwade/seed";
// Idempotent — skips tables that already have rows.
await seedClient(supabase, schema, { rowsPerTable: 20 });
When you ship, fakebase seed gen writes the same data to supabase/seed.sql. Full guide:
Fake Data Generation.
3. Read in a Server Component
Reads happen on the server during render — no API round-trip, no client JavaScript.
// app/posts/page.tsx
import { supabase } from "@/lib/fakebase";
export default async function PostsPage() {
const { data: posts, error } = await supabase
.from("posts")
.select("*")
.eq("published", true)
.order("created_at", { ascending: false });
if (error) return <p>Failed to load: {error.message}</p>;
return (
<ul>
{posts?.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
4. Write with a Server Action
Because the kernel is server-only, client writes go through a Server Action (or a Route Handler). The call shape is identical to Supabase.
// app/posts/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { supabase } from "@/lib/fakebase";
export async function createPost(formData: FormData) {
const { data, error } = await supabase
.from("posts")
.insert({ title: String(formData.get("title")), published: true })
.select();
revalidatePath("/posts");
return { data, error: error?.message ?? null };
}
Wire it to a <form action={createPost}> in a Client Component. That's the whole
read-on-server, write-through-an-action pattern.
Try all of this live, with no install, in the Playground — every action there shows the real
{ data, error }it returns.
5. Add auth
Auth uses local, GoTrue-shaped flows. Same surface as Supabase:
await supabase.auth.signUp({ email, password });
await supabase.auth.signInWithPassword({ email, password });
const { data } = await supabase.auth.getUser();
await supabase.auth.signOut();
Unsupported flows (OAuth, SSO, MFA, passkeys) return a structured CapabilityError instead of
silently doing nothing — so you always know what is and isn't real. See the
Compatibility Matrix.
6. Ship: export to real Supabase
Fakebase is designed to be thrown away. When you're ready:
fakebase migrate export --supabase # supabase/migrations/*.sql from your schema IR
fakebase seed export # supabase/seed.sql from your local data
fakebase types gen # database.types.ts (identical shape to Supabase's)
fakebase verify supabase # run the compatibility suite vs a real Supabase stack
Then swap the import — fakebase's createClient for @supabase/supabase-js's createClient —
and your app code stays the same. Full details in the Migration Guide.
Where to go next
- How it works — the kernel, adapters, and an anatomy-of-a-query walkthrough.
- Playground — run real queries against a live in-memory kernel.
- Architecture — the design and the three layers of fidelity.
- Security — the boundaries of the RLS/auth approximations (read before you ship).