How To Run Drizzle ORM on a D1 Database in Cloudflare Pages SvelteKit Application
Learn how to set up Drizzle ORM to build a schema for your D1 database in Cloudflare and build a simple project with it.
In this tutorial, you will learn:
- How to set up Drizzle ORM in a pre-existing Cloudflare Pages SvelteKit application bound to a D1 Database.
- How to generate and apply migrations to your D1 database.
- How to use Drizzle ORM in your application.
The tutorial focuses on adding Drizzle ORM to a pre-existing project. If you’re starting from scratch, use sv create and select it as a dependency during setup.
Prerequisites
To complete this tutorial, you need:
- A preexisting Cloudflare Pages SvelteKit application bound to a D1 Database.
- The
wranglerCLI.
There’s a lot hidden in the first prerequisite. Read how to add a D1 Database here:
Use a D1 Database in a Cloudflare Pages SvelteKit Application
Step 1 — Set up Drizzle ORM
First, install drizzle-orm and drizzle-kit with the following command:
npm install drizzle-orm
The drizzle-orm package will be used directly in your application as the wrapper layer for your database. The drizzle-kit package is local and is used to manage migrations.
Define your schema
Set up your schema.ts file inside of the lib directory of your SvelteKit project with:
mkdir -p src/lib/server/db
touch src/lib/server/db/schema.tsThis is where you’ll define your database schema. You can learn how to define your schema here. The schema below is an example we’ll use later in the tutorial:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const visitors = sqliteTable('visitors', {
id: integer('id').primaryKey({ autoIncrement: true }),
country: text('country'), // "US", "GB", etc.
city: text('city'), // "New York", "London"
timestamp: integer('timestamp', { mode: 'timestamp' }).notNull()
});Add the configuration
Next, add the drizzle kit config file to your root directory with:
touch drizzle.config.tsThen paste in the following:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/lib/server/db/schema.ts', // Point to your schema file
out: './drizzle', // Where SQL migrations will be saved
dialect: 'sqlite' // D1 is SQLite-based
});This tells drizzle-kit commands where to find your schema, where migrations are stored, and how to generate them. Don’t worry about creating the /drizzle directory at the root; it happens automatically.
Step 2 — Integrate Drizzle Kit with D1
In the Drizzle Kit docs, they tell you to migrate changes directly to Cloudflare. To do so, you need to collect and manage environment variables. Don’t do this, there’s a cleaner way with Cloudflare's wrangler tool.
Add the following scripts to package.json:
{
// ...
"scripts": {
// ...
"db:generate": "drizzle-kit generate",
"db:migrate:local": "wrangler d1 migrations apply <db-binding-name> --local",
"db:migrate:prod": "wrangler d1 migrations apply <db-binding-name> --remote",
// ...
},
// ...
}Make sure you replace: <db-binding-name> with the binding name of your D1 Database in your wrangler config file. For example, a commonly recommended binding is DB.
You’ll see how these scripts work in a moment.
Next, update your wrangler.jsonc database config to this:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "fullstackgaming-database-prod",
"database_id": "ecaf2fd9-9asdfh-ash9-a6c868fe6b78",
"migrations_dir": "./drizzle" // Add this option in
}
],
}Or if you’re using a .toml file, do this:
[[ d1_databases ]]
binding="DB"
database_name="fullstackgaming-database-prod"
database_id="ecaf2fd9-9asdfh-ash9-a6c868fe6b78"
migrations_dir="./drizzle" # Add this option inThis tells wrangler you’re using migrations to manage the database, and they can be found in the ./drizzle directory. You can read this resource for more information.
The one downside
You can’t use drizzle-kit studio with this method.
Step 3 — Working With Migrations
I’ll break down the workflow into several stages.
Generate a migration
Let’s generate the initial migration. There are two ways to do this:
npm run db:generateor
npm run db:generate -- --name <your-migration-name>The first one will generate a random name like:
0000_jazzy_tarot.sql0000_good_nicolaos.sql
These are the first two I generated. As you can see, the prefix 0000_ is always there, which remains the case in either method and should be left for ordering.
In the second method, you choose the name. For example:
npm run db:generate -- --name initialProduces: 0000_initial.sql. Giving migrations a meaningful name is considered good practice, particularly as you commit them, track them, and onboard people to your schema.
Whatever method you choose, if you use the Visitor schema.ts provided above, your migration file should look like this:
CREATE TABLE `visitors` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`country` text,
`city` text,
`timestamp` integer NOT NULL
);And you should now have ./drizzle directory with this structure:
📁 drizzle/
├── 📄 0000_initial.sql
├── 📁 meta/
│ ├── 📄 0000_snapshot.json
│ └── 📄 _journal.jsonApplying a local migration
In the scripts, you will have seen local and production migration options. This is one of the best bits of using wrangler for migrations, there is no configuration swap. You can easily push to both with simple commands.
Let’s start by updating the local database with:
npm run db:migrate:localThis will show you output like the following:
➜ full-stack-gaming git:(feat/add-drizzle-orm) ✗ npm run db:migrate:local
> full-stack-gaming@0.0.1 db:migrate:local
> wrangler d1 migrations apply DB --local
⛅️ wrangler 4.47.0 (update available 4.60.0)
─────────────────────────────────────────────
Resource location: local
Use --remote if you want to access the remote instance.
Migrations to be applied:
┌────────────────────────┐
│ name │
├────────────────────────┤
│ 0000_initial.sql │
└────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database DB (ecaf2fd9-9asdfh-ash9-a6c868fe6b78) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
┌────────────────────────┬────────┐
│ name │ status │
├────────────────────────┼────────┤
│ 0000_initial.sql │ ✅ │
└────────────────────────┴────────┘You can check this has worked by inspecting the SQLite database stored in the following directory:
.wrangler/state/v3/d1/miniflare-D1DatabaseObjectThe data is stored as binary, so you’ll need a special tool do this. One such tool is the SQLite Viewer, available in VSCode and forks (Cursor etc.).
Tidying up local
After you’ve finished testing locally, you may want to delete the test migrations you’ve made and the tables they’ve created.
You can do this by first going into drizzle/meta/_journal.json and deleting the entry for your migrations. Then deleting the corresponding snapshot, which will have the same prefix as the migration. For example: 0000_.
Finally, you can run the following commands:
rm drizzle <your-migration>.sql
You can extend this to drop as many test tables as you like.
You don’t have to drop the d1_migrations table, you could remove rows if you like.Applying a production migration
When you’re happy everything is working locally, you can apply the migration to production with:
npm run db:migrate:prodYou can check if it’s worked by finding the corresponding D1 Database in your Cloudflare dashboard.
There is no easy way to rollback a migration in Cloudflare D1. They use the forward rollback approach, where every rollback is just a further migration.
However, if you do find yourself in trouble and need to rollback to an earlier state, you can use this method:
Time Travel and backups · Cloudflare D1 docs
Step 4 — Use Drizzle ORM for a Visitor Book
Now we will wire everything up. We are going to build a “zero-input” guestbook that logs the user’s location automatically using Cloudflare’s edge data.
Create a new route:
mkdir -p src/routes/visitor
Server logic
Inside of src/routes/visitor/+page.server.ts paste the following code:
import { drizzle } from 'drizzle-orm/d1';
This connects your D1 database (platform.env.DB) with the request metadata (platform.cf). As you can see, we:
- Import the
visitorstable from theschema.tsfile. - Set up
drizzlewith the D1 database bindingDB. - Then
insertandselectvalues using thedrizzle-orminterface.
A small aside, to use platform.cf without errors, you’ll need to update your app.d.ts file:
import {
The UI
Create src/routes/+page.svelte. We use a simple form to trigger the server action.
<script>
export let data;
</script>
<div
style="width: 100%; margin: 20px auto;display: flex; flex-direction: column; align-items: center; justify-content: center;"
>
<h1 style="font-size: 2rem; margin-bottom: 1rem">Global Guestbook</h1>
<form method="POST" style="margin-bottom: 2rem">
<button
type="submit"
style="padding: 0.5rem 1rem; background: #6366f1; color: white; border: none; border-radius: 0.375rem; cursor: pointer"
>📍 I was here</button
>
</form>
<ul style="list-style: none; padding: 0">
{#each data.list as visitor}
<li style="margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem">
{#if visitor.country}
<img src="https://flagcdn.com/24x18/{visitor.country.toLowerCase()}.png" alt="flag" />
{/if}
<span>{visitor.city || 'Unknown'}{visitor.country ? ', ' + visitor.country : ''}</span>
</li>
{/each}
</ul>
</div>Step 5 — Run and Test It
Start your local development server:
npm run devOpen your browser, and you’ll see something like this:

Click the button a few times, and you’ll register your location in the database:

There you have it! You have a working application running on a D1 Database with Drizzle ORM.
Final Words
I hope you enjoyed this quick romp through Drizzle and D1 Databases. With the publication of this article, my series on building a site on Cloudflare is almost complete. All that’s left is product metric tracking with PostHog, stay tuned for that, and happy Drizzlin’!