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 beforeselect(...). 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.wasmfrom a public path your bundler can fetch at runtime (e.g.,/public/sql-wasm.wasmor 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.
| Setting | SQLite Web | Supabase |
|---|---|---|
VITE_VIBECODE_ADAPTER | sqlite | supabase |
| Extra env | none | VITE_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: trueand your DDL includes properFOREIGN 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).