Build your first app (Todos)

This hands‑on guide shows how to integrate vibecode‑db into a simple Todos app.
You’ll define a schema once, wire one client, and switch storage by swapping the adapter (SQLite Web ↔ Supabase) — no query rewrites.

Execution rule: In v1.0, .select(projection) executes the query. Chain filters/modifiers before select(...). Writes (insert, update, delete) execute immediately.


1) Install & pick your adapter

1# Web (WASM SQLite) or Supabase (cloud Postgres)
2npm i @vibecode-db/client @vibecode-db/sqlite-web
3# Supabase adapter is bundled in @vibecode-db/client (no extra package)

Set an env flag to toggle adapters at build/runtime (example for Vite):

1# .env
2VITE_VIBECODE_ADAPTER=sqlite   # or: supabase
3VITE_SUPABASE_URL=...          # only if using Supabase
4VITE_SUPABASE_ANON_KEY=...

2) Define your schema

Use the vibecode‑db DSL (vibecodeTable, col, references) to model tables and relations.
This generates a Zod bundle and TS types for safe CRUD everywhere.

1// db/schema.ts
2import { vibecodeTable, col, references, defineSchema } from '@vibecode-db/client'
3
4export const users = vibecodeTable('users', {
5  id: col.integer(),
6  name: col.varchar(),
7  email: col.varchar(),
8})
9
10export const todos = vibecodeTable('todos', {
11  id: col.varchar(),
12  title: col.varchar({ length: 256 }),
13  completed: col.boolean(),
14  created_at: col.timestamp(),
15  updated_at: col.timestamp(),
16  user_id: references(col.integer('user_id'), () => users.id), // M→1
17})
18
19export const db = defineSchema({ users, todos })

3) Compose your DBSpec

Provide the schema (required), relations (recommended), and optional seed for quick bootstrapping.

1// db/spec.ts
2import type { DBSpec } from '@vibecode-db/client'
3import { db } from './schema'
4
5export const dbSpec: DBSpec<typeof db.zodBundle.shape> = {
6  schema: db.zodBundle,
7  relations: db.relations,
8  seed: {
9    users: [
10      { id: 1, name: 'Ada',  email: 'ada@example.com' },
11      { id: 2, name: 'Alan', email: 'alan@example.com' },
12    ],
13    todos: [
14      {
15        id: 't1',
16        title: 'Wire the UI',
17        completed: false,
18        user_id: 1,
19        created_at: new Date(),
20        updated_at: new Date(),
21      },
22    ],
23  },
24}

4) SQLite migrations (DDL)

If you’re using sqlite, prepare a one‑time DDL array to create tables and indexes in SQLite.

1// db/migrations.ts
2export const migrations: string[] = [
3  `CREATE TABLE IF NOT EXISTS "users" (
4     "id"      INTEGER PRIMARY KEY,
5     "name"    TEXT,
6     "email"   TEXT
7   );`,
8
9  `CREATE TABLE IF NOT EXISTS "todos" (
10     "id"          TEXT PRIMARY KEY,
11     "title"       TEXT,
12     "completed"   INTEGER,
13     "created_at"  TEXT,
14     "updated_at"  TEXT,
15     "user_id"     INTEGER,
16     FOREIGN KEY("user_id") REFERENCES "users"("id")
17   );`,
18
19  `CREATE INDEX IF NOT EXISTS "todos_created_at_idx" ON "todos" ("created_at");`,
20]

Tip: keep FK enforcement on (see adapter option enableForeignKeys: true).


5) Create the client (choose adapter)

One client surface, two storage options.

1// db/client.ts
2import { createClient } from '@vibecode-db/client'
3import { SupabaseAdapter } from '@vibecode-db/client/adapters/supabase'
4import { SQLiteWebAdapter, type SQLiteWebAdapterOptions } from '@vibecode-db/sqlite-web'
5import { dbSpec } from './spec'
6import { migrations } from './migrations'
7
8const which = import.meta.env.VITE_VIBECODE_ADAPTER as 'sqlite' | 'supabase'
9
10const sqliteOpts: SQLiteWebAdapterOptions = {
11  wasmUrl: 'node_modules/sql.js/dist/sql-wasm.wasm', // ensure this asset is served
12  migrations,
13  enableForeignKeys: true,
14  seedBehavior: 'upsert',
15}
16
17export const db = createClient({
18  dbSpec,
19  adapter: (ctx) => {
20    if (which === 'supabase') {
21      return new SupabaseAdapter(ctx, {
22        url: import.meta.env.VITE_SUPABASE_URL as string,
23        key: import.meta.env.VITE_SUPABASE_ANON_KEY as string,
24      })
25    } else {
26      return new SQLiteWebAdapter(ctx, sqliteOpts)
27    }
28  },
29})

WASM note: serve sql-wasm.wasm from a public path your bundler can fetch at runtime (e.g., /public/sql-wasm.wasm or the node_modules URL above).


6) Query the app data

A few idioms you’ll use right away in your React screens.

1// Read users (projection + order)
2const users = await db
3  .from('users')
4  .order('name', { ascending: true })
5  .select('id, name, email')
1// Read todos for a user (one-level M→1 relation in projection)
2const todos = await db
3  .from('todos')
4  .eq('user_id', 1)
5  .order('created_at', { ascending: false })
6  .limit(10)
7  .select('id, title, completed, user_id, created_at, updated_at, users(name, email)')
1// Insert a todo
2await db.from('todos').insert({
3  id: crypto.randomUUID(),
4  title: 'New task',
5  completed: false,
6  user_id: 1,
7  created_at: new Date(),
8  updated_at: new Date(),
9})
1// Toggle (update) a todo
2await db.from('todos').eq('id', 't1').update({ completed: true, updated_at: new Date() })
1// Delete a todo
2await db.from('todos').eq('id', 't1').delete()

7) Adapter switch (no UI changes)

Toggle via env and rebuild — the queries above stay identical.

SettingSQLite WebSupabase
VITE_VIBECODE_ADAPTERsqlitesupabase
Extra envnoneVITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY

8) Notes & troubleshooting

  • Relations (SQLite): current support is “SELECT list + LEFT JOINs for one‑level many‑to‑one”. Deep chains and inline 1→M expansions require multiple queries or a view.
  • WASM path: if the browser can’t fetch sql-wasm.wasm, verify the public URL.
  • FKs: make sure enableForeignKeys: true and your DDL includes proper FOREIGN KEY (...) REFERENCES ....
  • Supabase RLS: ensure policies allow the reads/writes you’re attempting.

That’s it

You now have a portable data layer for your Todos UI. Keep the schema and queries the same while switching adapters for local‑first (SQLite) or cloud (Supabase).