Database

Each app comes with its own SQL database, exposed as env.DB. It's SQLite at the edge, with the standard prepared-statement API. You define the schema; the platform reserves a small set of table names for its own managed features.

Querying with env.DB

Use prepared statements with bound parameters, never string interpolation:

// api/notes.js
export default {
  async fetch(request, env, ctx) {
    const user = await env.AUTH.getUser(request);
    if (!user) return new Response("Unauthorized", { status: 401 });

    // read many rows
    const { results } = await env.DB
      .prepare("SELECT id, body, created_at FROM notes WHERE user_id = ?")
      .bind(user.id)
      .all();

    return Response.json(results);
  },
};

The common methods:

  • .prepare(sql).bind(...args), build a parameterized statement.
  • .all(), all rows ({ results }).
  • .first(), the first row, or null.
  • .run(), execute a write (insert / update / delete).
  • env.DB.batch([...statements]), run several statements atomically.

Schema & migrations

Define and evolve your schema with SQL files under migrations/. Keep them ordered (a numeric prefix is the convention) so they apply deterministically:

-- migrations/0001_notes.sql
CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT NOT NULL,
  body TEXT NOT NULL,
  created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);

You can review what an app has defined with the list_migrations MCP tool. In practice you just ask your assistant for the schema you want and it writes the migration.

Numbering across branches. The prefix has to be unique across all branches, not just your own, count from what every branch has, not from your branch’s last file. If two branches both add 0042_*.sql, the merge is refused until one is renumbered. When several agents work in parallel, don’t guess: call manage_branch (action: reserve_migration) to claim the next free number atomically before you write the file, or read migrationCoordination.nextAvailable from manage_branch (action: list). See Working in parallel.

Reserved: the _cr_* namespace

Table names starting with _cr_ are owned by the platform, they back managed features like env.AUTH (_cr_users, _cr_sessions) and your app's own MCP OAuth server (_cr_oauth_*). They're created lazily on first use.

Don't create, write to, or migrate _cr_* tables yourself, name your own tables anything else. Use the env.AUTH methods to read or change user data.

Preview vs production data

A production (main) deploy uses the real app database. Branch and preview deploys run against a separate throwaway database, so work-in-progress can't corrupt live data, see Branches, PRs & merges.

By default a preview database starts empty, no production data is copied into it, so your customers' data is never duplicated into a sandbox. Seed whatever test rows you need. If you genuinely want realistic data in previews, an owner/admin can opt in with set_preview_data_mode ("light" for login/account data, "full" for app rows too).