Core Concepts — Client & Context

This section clarifies what the client is and what “context” (ctx) carries so every adapter can initialize consistently while your query API stays identical across runtimes.


3.1 What is the Client?

  • The Client is the runtime interface you call after createClient(...).
  • It exposes the fluent query builder (from().where() … .select()), type‑safe via your schema.
  • Rule of thumb: chain filters/modifiers first, then call .select(...) to execute.
1// Execute after filters → .select(...)
2const todos = await db
3  .from('todos')
4  .where({ user_id: 1 })
5  .select('id, title, users(name)') // nested projection from related table

3.1 Context (ctx)

  • Context is passed into your adapter factory: (ctx) => new SomeAdapter(ctx, options).
  • It always includes your DBSpec, so the adapter can prepare storage, run migrations (SQLite), and enforce relations.

DBSpec (required vs optional):

  • schemarequired (Zod bundle generated from your vibecode‑db schema DSL)
  • relationsrequired if you model FKs (recommended by default)
  • seed — optional (boot data for dev/demos)
  • meta — optional (adapter‑specific metadata)
1// db/spec.ts
2import type { DBSpec } from '@vibecode-db/client'
3import { db } from './schema' // defineSchema(...)
4
5export const dbSpec: DBSpec<typeof db.zodBundle.shape> = {
6  schema: db.zodBundle,
7  relations: db.relations,
8  // seed, meta (optional)
9}

Creating the Client (environment-agnostic pattern)

Use separate files per runtime; the only thing that changes is the adapter.

1// db/client.web.ts (Web — sqlite-web)
2import { createClient } from '@vibecode-db/client'
3import { SQLiteAdapter } from '@vibecode-db/sqlite-web'
4import { dbSpec } from './spec'
5import { migrations } from './migrations' // string[] of SQL
6
7export const db = createClient({
8  dbSpec,
9  adapter: (ctx) => new SQLiteAdapter(ctx, {
10    wasm: { wasmUrl: '/sql-wasm.wasm' },
11    migrations,
12  }),
13})
1// db/client.expo.ts (Expo — sqlite-expo)
2import { createClient } from '@vibecode-db/client'
3import { SQLiteAdapter } from '@vibecode-db/sqlite-expo'
4import { dbSpec } from './spec'
5import { migrations } from './migrations'
6
7export const db = createClient({
8  dbSpec,
9  adapter: (ctx) => new SQLiteAdapter(ctx, {
10    native: { dbName: 'vibecode.db' },
11    migrations,
12  }),
13})
1// db/client.supabase.ts (Supabase — adapter included in @vibecode-db/client)
2import { createClient as createVC } from '@vibecode-db/client'
3import { dbSpec } from './spec'
4
5export const db = createVC({
6  dbSpec,
7  adapter: (ctx) => /* new SupabaseAdapter(ctx, { url, key }) */ ({} as any),
8})

Note: We intentionally don’t document sqlite-core here (internal/shared). You rarely interact with it directly.


Why this split works

  • Stable API surface: Your app code calls the same query builder everywhere.
  • Predictable startup: Adapters receive the same ctx/DBSpec, so they can set up storage, migrations, and constraints consistently.
  • Easy swapping: Moving from Web to Expo or Supabase is swapping the adapter file—your queries stay intact.

Summary

  1. Build a DBSpec from your schema (+ relations).
  2. Call createClient({ dbSpec, adapter: (ctx) => new Adapter(ctx, options) }).
  3. Use the same fluent query builder across environments; .select(...) executes.

That’s Client & Context in a nutshell: one contract that unlocks multiple runtimes without rewriting queries.