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, ornull..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.
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.
_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).