Skip to content

Deploy to Cloudflare

Cloudflare Workers provides a fast, globally distributed runtime for EmDash. This guide covers deploying with D1 for the database and R2 for media storage.

  • A Cloudflare account
  • Wrangler CLI installed (npm install -g wrangler)
  • Authenticated with Cloudflare (wrangler login)
Terminal window
wrangler d1 create emdash-db

Note the database_id from the output.

Terminal window
wrangler r2 bucket create emdash-media

Create wrangler.jsonc in your project root:

wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-emdash-site",
"compatibility_date": "2025-01-15",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db",
"database_id": "your-database-id",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media",
},
],
}

Update your Astro configuration to use D1 and R2:

astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import emdash, { r2 } from "emdash/astro";
import { d1 } from "emdash/db";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});

Generate and apply the database schema.

Terminal window
npx emdash init --database ./data.db
Terminal window
wrangler d1 migrations apply emdash-db

If you don’t have migration files, apply the core schema directly:

Terminal window
wrangler d1 execute emdash-db --file=./node_modules/emdash/migrations/0001_core.sql

Deploy to Cloudflare Workers:

Terminal window
wrangler deploy

Your site is now live at https://my-emdash-site.<your-subdomain>.workers.dev.

For globally distributed sites, enable D1 read replication to route read queries to nearby replicas instead of always hitting the primary database. This significantly reduces latency for visitors far from the primary region.

astro.config.mjs
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
storage: r2({ binding: "MEDIA" }),
}),

You also need to enable read replication on the D1 database itself in the Cloudflare dashboard or via the REST API.

See Database Options — Read Replicas for session modes and how bookmark-based consistency works.

Add a custom domain in the Cloudflare dashboard:

  1. Go to Workers & Pages > your worker
  2. Click Custom Domains > Add Custom Domain
  3. Enter your domain and follow the DNS setup instructions

To serve media directly from R2 (recommended for performance):

  1. In the Cloudflare dashboard, go to R2 > your bucket
  2. Click Settings > Public access
  3. Enable public access and note the public URL
  4. Update your storage config:
astro.config.mjs
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxx.r2.dev"
}),

If your organization uses Cloudflare Access, you can use it as your authentication provider instead of passkeys. This provides SSO with your existing identity provider.

astro.config.mjs
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
auth: {
cloudflareAccess: {
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
"Admins": 50,
"Editors": 40,
},
},
},
}),

See the Authentication guide for full configuration options.

EmDash requires certain secrets for authentication and preview functionality.

VariablePurpose
EMDASH_AUTH_SECRETSigns session cookies and auth tokens. Required for production.
EMDASH_PREVIEW_SECRETSigns preview URLs for draft content. Required for preview functionality.

Generate secure secrets:

Terminal window
npx emdash auth secret

Set secrets via Wrangler:

Terminal window
wrangler secret put EMDASH_AUTH_SECRET
wrangler secret put EMDASH_PREVIEW_SECRET

Access environment variables in your configuration using import.meta.env or the Cloudflare env binding.

Deploy a preview branch:

Terminal window
wrangler deploy --env preview

Add an environment section to wrangler.jsonc:

{
"env": {
"preview": {
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db-preview",
"database_id": "your-preview-db-id",
},
],
},
},
}

Verify the binding name in wrangler.jsonc matches your database configuration:

// Must match: d1({ binding: "DB" })
"binding": "DB"

Check that the R2 bucket is correctly bound:

// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"

D1 migrations run via Wrangler, not at runtime. If you see schema errors:

  1. Check that migrations were applied: wrangler d1 migrations list emdash-db
  2. Re-apply if needed: wrangler d1 migrations apply emdash-db