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.

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 & AI merges.