Skip to content

Going full-stack on Astro with Cloudflare D1 and Drizzle

• ~5 min read • Updated

A step-by-step guide to adding a back-end to your Astro project using Cloudflare D1 and Drizzle ORM

I recently wrote about adding /shared links to my website, and wanted to document how I went about setting it up for anyone who wants to do the same!

Installing Astro #

This is pretty straightforward — running the following:

npm create astro@latest
  1. Select Empty for the template
  2. Opt-in to the strictest TypeScript
  3. Defaults are fine for everything else

Then you can cd into your project and run npm run dev

Adding Cloudflare adapter #

In your project, you can now run:

npx astro add cloudflare

Say yes to everytyhing, then commit everything and push it up to Github.

Deploying to Pages #

Head over to the Create Pages Application page in the Cloudflare dashboard and click “Connect to git” to create a Pages application using the Github repo.

Be sure to select the Astro Framework preset!

Install wrangler and log in #

If you haven’t already done this, install wrangler and log in by running:

npm i -g wrangler
wrangler login

Create D1 databases #

We’re going to create two databases, one for production, and one for preview builds.

To do this, run the following commands:

wrangler d1 create d1-demo-prod-db
wrangler d1 create d1-demo-preview-db

Create wrangler.toml file #

We’re going to need a wrangler.toml file with the database_id from each of the databases we just created.

# wrangler.toml
node_compat = true

[[d1_databases]]
binding = "DB"
database_name = "d1-demo-prod-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
preview_database_id = "DB"

[env.preview]
name = "preview"
[[env.preview.d1_databases]]
binding = "DB"
database_name = "d1-demo-preview-db"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

Update .gitignore #

echo .wrangler >> .gitignore
echo .drizzle.env >> .gitignore

Update astro.config.ts #

We need to add the D1 binding like this:

// astro.config.ts
import { defineConfig } from "astro/config";

import cloudflare from "@astrojs/cloudflare";

// https://astro.build/config
export default defineConfig({
	output: "server",
	adapter: cloudflare({
		platformProxy: {
			enabled: true,
		},
	}),
});

Install Drizzle dependencies #

Run the following commands:

npm i drizzle-orm
npm i -D better-sqlite3 drizzle-kit cross-env @types/node

Create drizzle.config.ts #

As of this writing, there’s an open issue for better support of drizzle-studio with D1 projects, so we’re going to use a workaround for now. This is what our drizzle.config.ts file will look like:

// drizzle.config.ts
import type { Config } from "drizzle-kit";

const { LOCAL_DB_PATH, DB_ID, D1_TOKEN, CF_ACCOUNT_ID } = process.env;

// Use better-sqlite driver for local development
export default LOCAL_DB_PATH
	? ({
			schema: "./src/schema.ts",
			dialect: "sqlite",
			dbCredentials: {
				url: LOCAL_DB_PATH,
			},
		} satisfies Config)
	: ({
			schema: "./src/schema.ts",
			out: "./migrations",
			dialect: "sqlite",
			driver: "d1-http",
			dbCredentials: {
				databaseId: DB_ID!,
				token: D1_TOKEN!,
				accountId: CF_ACCOUNT_ID!,
			},
		} satisfies Config);

Create your schema #

Let’s go ahead and create src/schema.ts

// src/schema.ts
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const linkShare = sqliteTable("linkShare", {
	id: integer("id").primaryKey({
		autoIncrement: true,
	}),
	url: text("url").notNull(),
	title: text("title").notNull(),
	remark: text("remark"),
	created: integer("created", {
		mode: "timestamp_ms",
	})
		.notNull()
		.$defaultFn(() => new Date()),
	modified: integer("modified", {
		mode: "timestamp_ms",
	})
		.notNull()
		.$defaultFn(() => new Date()),
	deleted: integer("deleted", {
		mode: "timestamp_ms",
	}),
});

Add scripts to package.json #

Let’s add the following npm scripts to our package.json’s scripts.

Be sure to update the DB_ID values according to your preview and prod ID’s generated in the previous steps!

{
	"scripts": {
		"db:generate": "drizzle-kit generate",
		"db:migrate:local": "wrangler d1 migrations apply d1-demo-prod-db --local",
		"db:migrate:prod": "wrangler d1 migrations apply d1-demo-prod-db --remote",
		"db:migrate:preview": "wrangler d1 migrations apply --env preview d1-demo-preview-db --remote",
		"db:studio:local": "LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio",
		"db:studio:preview": "source .drizzle.env && DB_ID='yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' drizzle-kit studio",
		"db:studio:prod": "source .drizzle.env && DB_ID='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' drizzle-kit studio"
	}
}

Generate migrations #

Now we’re going to use Drizzle to generate the first migration to apply!

npm run db:generate

Apply the migration locally #

We will run

npm run db:migrate:local

This will apply the migration to our local database!

Interact with local DB using Drizzle Studio #

Now we can inspect the local database by running

npm run db:studio:local

Try inserting a row into the linkShares table adding only the url and title!

Add DB to locals #

To generate our D1 binding types, we need to run:

npx wrangler types

Then update src/env.d.ts as follows:

// src/env.d.ts

/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

type Runtime = import("@astrojs/cloudflare").Runtime<Env>;

declare namespace App {
	interface Locals extends Runtime {}
}

In src/pages/index.astro, you can now query the D1 database:

---
// src/pages/index.astro
import { desc } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import { linkShare } from "../schema";

const db = drizzle(Astro.locals.runtime.env.DB);

const links = await db
	.select()
	.from(linkShare)
	.orderBy(desc(linkShare.created));
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<h1>Shared Links</h1>
		<ul>
			{
				links.map((link) => (
					<li>
						<a href={link.url}>{link.title}</a>
					</li>
				))
			}
		</ul>
	</body>
</html>

At this point, you should be able to run the local dev server again:

npm run dev

Add bindings in Pages project #

  1. In the Cloudflare dashboard, go to your Pages projects and open your project.
  2. Go to Settings > Functions > D1 database bindings
  3. Under the “Production” tab, click “Add binding”, and name it DB. Bind this to the d1-demo-prod-db database we created earlier.
  4. Under the “Preview” tab, click “Add binding”, and name it DB. Bind this to the d1-demo-preview-db database we created earlier.

Run migrations for preview and prod #

npm run db:migrate:preview
npm run db:migrate:prod

Create .drizzle.env #

Since prod and preview are going to use the d1-http adapter, we need to provide these tokens in the .drizzle.env file. You can create this token here.

export D1_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaa
export CF_ACCOUNT_ID=bbbbbbbbbbbbbbbbbbbbbbbbbbbb

Add data to preview and prod environments #

Use Drizzle Studio again to populate some data in the preview environment by running:

npm run db:studio:preview

Then do the same for prod by running:

npm run db:studio:prod

Commit and open a PR #

It’s time to test the preview build! Let’s create a branch and push it up.

git checkout -b testing-preview-builds
git add .
git commit -m "D1 and Drizzle setup"
git push -u origin HEAD

The preview build should work! Go ahead and merge it, and prod should work too! 🎉

That’s all! #

You can check out the completed code on Github.

Share this post

📧 Email