Deployment
Deploying to Cloudflare Pages
Stedefast is designed to run on Cloudflare Pages. Your static site is served from the CDN edge, and dynamic modules run as Pages Functions backed by D1, KV, and R2.
Prerequisites
- A Cloudflare account
- Wrangler CLI installed and authenticated:
pnpm add -g wrangler wrangler login
1. Install the Cloudflare provider
The Cloudflare provider wraps CF Workers bindings (D1, KV, R2, Email Routing) in the provider interfaces that Stedefast modules use.
pnpm add @stedefast/provider-cloudflare
2. Register the provider in your config
// stedefast.config.ts
import { defineConfig } from "@stedefast/core";
import { createCloudflareProvider } from "@stedefast/provider-cloudflare";
import { CommentsModule } from "@stedefast/module-comments";
import { ClapsModule } from "@stedefast/module-claps";
export default defineConfig({
siteTitle: "My Site",
baseUrl: "https://my-site.pages.dev",
contentDir: "./content",
outputDir: "./dist",
theme: "./theme",
// Wire up the Cloudflare provider — modules access services through this
providers: createCloudflareProvider({
dbBinding: "DB", // Name of your D1 binding in wrangler.toml
kvBinding: "KV", // Name of your KV namespace binding
storageBinding: "STATIC_R2", // Name of your R2 bucket binding
}),
modules: [
CommentsModule({ requireApproval: true }),
ClapsModule({ maxClapsPerPage: 50 }),
],
cloudflare: {
projectName: "my-site",
d1Databases: [
{ binding: "DB", databaseId: "YOUR_D1_DATABASE_ID" }
],
kvNamespaces: [
{ binding: "KV", namespaceId: "YOUR_KV_NAMESPACE_ID" }
],
r2Buckets: [
{ binding: "STATIC_R2", bucketName: "my-site-static" }
],
},
});
The createCloudflareProvider() call tells Stedefast how to create env.providers.db, env.providers.kv, and env.providers.storage at runtime from your Cloudflare bindings. See Provider Architecture for how this works.
3. Create your Cloudflare resources
Use Wrangler to create the resources, then paste the IDs into your config.
# D1 database (for comments, analytics, contact form, newsletter)
wrangler d1 create my-site-db
# → database_id = "abc123..."
# KV namespace (for claps, reactions)
wrangler kv:namespace create KV
# → id = "def456..."
# R2 bucket (optional — for media assets and regenerated JSON)
wrangler r2 bucket create my-site-static
If you're not using a module that requires a particular resource, skip creating it.
4. Set environment variables
For local development, create a .dev.vars file:
ENVIRONMENT=development
SITE_BASE_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-local-secret-at-least-32-chars
BETTER_AUTH_URL=http://localhost:3000
For production, set secrets via the Cloudflare dashboard (Pages → your project → Settings → Environment variables) or Wrangler:
wrangler pages secret put BETTER_AUTH_SECRET
wrangler pages secret put BETTER_AUTH_URL
Environment variable reference
| Variable | Required | Description |
|---|---|---|
ENVIRONMENT |
Yes | development or production |
SITE_BASE_URL |
Yes | Your site's canonical URL |
BETTER_AUTH_SECRET |
Yes | BetterAuth signing secret (≥32 chars) |
BETTER_AUTH_URL |
Yes | Same as SITE_BASE_URL |
TURNSTILE_SECRET_KEY |
Contact / Newsletter | Cloudflare Turnstile secret key |
NEWSLETTER_SECRET |
Newsletter | HMAC key for unsubscribe tokens |
RESEND_API_KEY |
Email via Resend | Resend API key |
CF bindings (D1, KV, R2) are declared in wrangler.toml — not environment variables — and reach modules through the provider layer.
5. Deploy
pnpm stedefast deploy
This command:
- Runs
stedefast build(all 7 pipeline stages) - Generates
wrangler.tomlfrom yourcloudflareconfig - Applies pending D1 migrations from registered modules
- Deploys via
wrangler pages deploy dist/
On first run, Wrangler will prompt you to link or create a Pages project.
Skip rebuild (CI)
pnpm stedefast deploy --skip-build
Useful in CI when build and deploy are separate steps:
# .github/workflows/deploy.yml
- run: pnpm stedefast build
- run: pnpm stedefast deploy --skip-build
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
6. Verify
After deploying, confirm:
- Your site loads at
https://<project>.pages.dev - Module endpoints work:
GET https://<project>.pages.dev/_modules/claps/count?pageId=test - The admin panel is accessible at
https://<project>.pages.dev/admin
Generated wrangler.toml
stedefast deploy writes wrangler.toml to your project root from your stedefast.config.ts:
name = "my-site"
compatibility_date = "2025-04-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = "./dist"
[[d1_databases]]
binding = "DB"
database_name = "my-site-db"
database_id = "abc123..."
[[kv_namespaces]]
binding = "KV"
id = "def456..."
[[r2_buckets]]
binding = "STATIC_R2"
bucket_name = "my-site-static"
Configuring email delivery
Modules that send email (contact form, newsletter) use env.providers.email. Two backends are supported:
Cloudflare Email Routing (free)
providers: createCloudflareProvider({
emailProvider: "email-routing",
sendEmailBinding: "SEND_EMAIL",
}),
Add to wrangler.toml:
[[send_email]]
binding = "SEND_EMAIL"
Set the destination address in the Cloudflare dashboard under Email → Email Routing.
Resend
providers: createCloudflareProvider({
emailProvider: "resend",
sendEmailBinding: "RESEND_API_KEY",
}),
Then set the secret:
wrangler pages secret put RESEND_API_KEY
Local development
pnpm stedefast dev --wrangler
Runs your site through wrangler pages dev, giving you real D1, KV, and R2 emulation locally. .dev.vars is loaded automatically.
Without --wrangler, stedefast dev starts a plain Node.js server — modules that depend on CF bindings degrade gracefully (returning empty data), but the site renders fully.