EmDash has two deployment shapes. On Node.js you use SQLite, libSQL or PostgreSQL plus the filesystem or S3. On Cloudflare Workers, D1 stores content and R2 stores media. This site uses the Cloudflare path, and here's the setup.
Scaffold for Cloudflare
bun create emdash@latest my-site --template cloudflare:blog --pm bunThe Astro config wires EmDash to the bindings by name:
import cloudflare from "@astrojs/cloudflare";
import { d1, r2 } from "@emdash-cms/cloudflare";
import emdash from "emdash/astro";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
emdash({
siteUrl: "https://example.com",
database: d1({ binding: "DB", session: "auto" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});Pin your resources
Create the database, bucket and session namespace yourself and pin their IDs in wrangler.jsonc. That keeps every deploy pointed at the same data.
bunx wrangler d1 create my-site
bunx wrangler r2 bucket create my-site-media
bunx wrangler kv namespace create my-site-session{
"d1_databases": [
{ "binding": "DB", "database_name": "my-site", "database_id": "<id>" }
],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-site-media" }],
"kv_namespaces": [{ "binding": "SESSION", "id": "<id>" }]
}The SESSION namespace backs Astro sessions, which the admin login uses.
Secrets
Keep EMDASH_ENCRYPTION_KEY out of your repo and your terminal history. Pipe it straight from .env into Wrangler:
grep EMDASH_ENCRYPTION_KEY .env | cut -d= -f2- | bunx wrangler secret put EMDASH_ENCRYPTION_KEYGotcha 1: passkeys are bound to the hostname
EmDash admin logins use passkeys, and a passkey belongs to one hostname. Decide on the final domain before anyone registers, set it as siteUrl, and 301 every other host (www, *.workers.dev) to it in middleware.
Gotcha 2: routes turn off workers.dev
The moment you add custom-domain routes, Wrangler disables the workers.dev URL unless you say otherwise:
{
"workers_dev": true,
"routes": [
{ "pattern": "example.com", "custom_domain": true },
{ "pattern": "www.example.com", "custom_domain": true }
]
}Ship it
bun run deployThat runs astro build && wrangler deploy. Open /_emdash/admin on your real domain, register your passkey, and you're live.



