Cloudflare D1
Cloudflare D1 is a managed SQLite database running at the edge, accessed through the Workers binding API.
What create-faster adds
D1 requires --deployment cloudflare — the DB binding is injected by the Workers runtime, so there is no connection string. create-faster wires this binding end-to-end across every generated file.
wrangler.jsonc — D1 binding declaration:
"d1_databases": [
{
"binding": "DB",
"database_name": "{appName}-db",
"database_id": "REPLACE_WITH_D1_DATABASE_ID",
"migrations_dir": "drizzle"
}
]Replace REPLACE_WITH_D1_DATABASE_ID with the id returned by wrangler d1 create.
src/lib/db/index.ts — factory, not singleton:
D1 is injected per request, not available at module load time. The generated client is a createDb factory that accepts the binding:
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
export function createDb(d1: D1Database) {
return drizzle(d1, { schema });
}
export type Database = ReturnType<typeof createDb>;- Single repo:
src/lib/db/index.ts - Turborepo:
packages/db/src/index.ts
src/lib/env.ts (Next.js only) — getEnv seam:
For Next.js apps, a getEnv helper retrieves the binding from the OpenNext Cloudflare context:
import 'server-only';
import { getCloudflareContext } from '@opennextjs/cloudflare';
export type Env = { DB: D1Database };
export async function getEnv(): Promise<Env> {
const { env } = await getCloudflareContext({ async: true });
return { DB: env.DB };
}Call getEnv() in Server Components or Route Handlers, then pass env.DB to createDb:
const env = await getEnv();
const db = createDb(env.DB);src/lib/server.ts (Next.js only) — per-request getDb/getAuth:
When the app also selects an ORM (and optionally Better Auth), create-faster generates an app-side accessor that composes the binding into ready-to-use per-request instances:
import 'server-only';
import { createDb } from '@/lib/db';
import { createAuth } from '@/lib/auth/auth';
import { getEnv } from '@/lib/env';
export async function getDb() {
return createDb((await getEnv()).DB);
}
export async function getAuth() {
return createAuth(await getDb());
}Better Auth & tRPC on D1
Because D1 has no module-level db singleton, Better Auth and tRPC are wired per request instead of importing a singleton:
- Better Auth (
auth.ts) exports acreateAuth(db: Database)factory (with the shared config) instead ofexport const auth. The auth route handler resolves it per request viaawait getAuth(). - tRPC (
init.ts/packages/api/src/trpc.ts) takes thedbin its context and buildsauth = createAuth(db)insidecreateTRPCContext. The Server Component caller and the route handler supply it withdb: await getDb().
This is fully supported in both single-repo and turborepo modes:
bunx create-faster myapp \
--app web:nextjs:trpc,better-auth \
--database d1 \
--orm drizzle \
--deployment cloudflaredrizzle.config.ts — sqlite dialect with d1-http prod branch:
The config uses the sqlite dialect locally (pointing at the miniflare D1 file) and switches to the d1-http driver in production:
const isProduction = process.env.NODE_ENV === 'production';
export default defineConfig({
dialect: 'sqlite',
...(isProduction
? {
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID,
token: process.env.CLOUDFLARE_API_TOKEN,
},
}
: { dbCredentials: { url: getLocalD1DB() ?? '' } }),
});Schema uses sqliteTable from drizzle-orm/sqlite-core — identical to the SQLite option. Schemas carry over unchanged between local and D1.
Environment Variables
Generated in .env.example (turborepo: packages/db/.env.example and apps/{appName}/.env.example for each app with D1; single repo: root .env.example):
CLOUDFLARE_ACCOUNT_ID="" # Cloudflare account id (drizzle-kit d1-http, prod migrations)
CLOUDFLARE_D1_DATABASE_ID="" # D1 database id from `wrangler d1 create`
CLOUDFLARE_API_TOKEN="" # Cloudflare API token with D1 edit permissionThese are only needed for db:migrate:remote (production migrations via d1-http). Local development uses the miniflare SQLite file automatically.
Workflow Scripts
| Script | Purpose |
|---|---|
db:generate | Generate migration SQL from schema changes (drizzle-kit generate) |
db:migrate / db:migrate:local | Apply migrations to the local D1 |
db:migrate:remote | Apply migrations to the production D1 database |
local-setup | First-time local setup: migrate + seed |
Single repo — everything is co-located at the project root, so migrate runs against the root wrangler.jsonc and the local state at ./.wrangler.
Turborepo — the migration SQL lives in packages/db/drizzle, but the D1 binding lives in the consuming app's wrangler.jsonc. The migrate scripts (in packages/db) therefore point wrangler at that app's config and persist to a single shared state dir at the workspace root:
wrangler --config ../../apps/<app>/wrangler.jsonc d1 migrations apply DB --local --persist-to ../../.wranglerThe app's wrangler.jsonc sets migrations_dir to ../../packages/db/drizzle, and opennextjs-cloudflare preview runs with the same --persist-to ../../.wrangler — so migrate, preview, and drizzle.config.ts all read and write the one local database. Run turbo db:migrate (or bun run db:migrate at the root) to apply migrations across the workspace.
Local development
--persist-to <dir> stores the local miniflare D1 under <dir>/v3/d1/. The generated drizzle.config.ts auto-discovers the latest SQLite file in .wrangler/v3/d1/ (root .wrangler/v3/d1/ in turborepo), so db:push and db:studio work without any env var once migrations have been applied.
Manual smoke test after local-setup:
bun run db:migrate # apply migrations to the local D1
bun run preview # build + start the Worker with the local D1 bindingORM constraint
D1 only works with Drizzle. --database d1 --orm prisma is rejected at validation time, and the interactive ORM prompt only offers compatible options.
bunx create-faster myapp \
--app myapp:nextjs \
--database d1 \
--orm drizzle \
--deployment cloudflare
