Skip to content
FLAVIO COPES
flaviocopes.com
2026

Astro SSR with D1 and Drizzle ORM

By Flavio Copes

Wire Cloudflare D1 into an Astro SSR app with Drizzle ORM. Bindings, schema, migrations, and local dev with Miniflare in one setup guide.

~~~

Astro SSR on Cloudflare Workers gives you a full server app at the edge. D1 gives you SQLite. Drizzle gives you typed queries.

That’s the stack I used for StackPlan. Here’s how the pieces connect.

Bind D1 in wrangler.jsonc

Create the database once:

npx wrangler d1 create stackplan

Wrangler prints an ID. Add it to your config:

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "stackplan",
      "database_id": "f2b1c3d4-1234-4a5b-8c9d-0e1f2a3b4c5d",
      "migrations_dir": "drizzle"
    }
  ]
}

binding is the name you use in code — env.DB. migrations_dir tells Wrangler where your SQL files live.

Define the schema with Drizzle

Drizzle models SQLite tables in TypeScript. Each table is a sqliteTable:

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const reports = sqliteTable('reports', {
  id: text('id').primaryKey(),
  userId: text('user_id'),
  status: text('status').notNull().default('pending'),
  createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
})

Put all tables in one schema file. Export them. Drizzle uses this file for both queries and migration generation.

Create the Drizzle client

One helper wraps the D1 binding:

import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'

export function createDb(d1) {
  return drizzle(d1, { schema })
}

Pass env.DB every time you need the database.

Get the binding in Astro routes

Older Astro Cloudflare setups used Astro.locals.runtime.env.DB. On Astro 7 with @astrojs/cloudflare v14, the supported path is the cloudflare:workers module:

import { env } from 'cloudflare:workers'
import { createDb } from '../db'

export const GET = async () => {
  const db = createDb(env.DB)
  const rows = await db.select().from(reports).limit(10)
  return Response.json(rows)
}

This works in astro dev (Miniflare simulates D1 locally) and in production. Secrets from .dev.vars load automatically during dev.

Generate migrations with drizzle-kit

Point Drizzle Kit at your schema:

// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'sqlite',
  driver: 'd1-http',
})

When you change the schema, generate SQL:

npm run db:generate
# drizzle-kit generate

Drizzle writes numbered files into ./drizzle/. Commit them. They’re your migration history.

Apply migrations

Local dev (Miniflare’s local D1):

npx wrangler d1 migrations apply stackplan --local

Production:

npx wrangler d1 migrations apply stackplan --remote

I keep both as npm scripts — db:migrate:local and db:migrate:remote. Run local after every schema change before you test. Run remote before deploy.

Wrangler tracks which migrations ran. It won’t re-apply old ones.

Local dev workflow

  1. Edit src/db/schema.ts
  2. npm run db:generate
  3. npm run db:migrate:local
  4. npm run dev

Miniflare persists local D1 data between restarts. Your tables behave like production, on your laptop.

Optional: seed data with wrangler d1 execute stackplan --local --file=./seed.sql.

Querying

Drizzle queries look like SQL written in TypeScript:

import { eq } from 'drizzle-orm'
import { reports } from '../db/schema'

const row = await db
  .select()
  .from(reports)
  .where(eq(reports.id, reportId))
  .limit(1)

You get autocomplete on column names. No raw string typos.

What to watch for

D1 batches are not full Postgres transactions. Drizzle’s db.batch([...]) maps to D1’s batch API — use it when you need two writes to succeed or fail together.

Keep migrations small and review the generated SQL before applying remote. D1 is SQLite — some ALTER operations need workarounds.

That’s the whole loop: schema in TypeScript, migrations in git, local D1 in dev, remote D1 in prod. No connection pool, no ORM config file on the server.

Tagged: Database · All topics
~~~

Related posts about database: