Stedefast

Deployment

Deploying to Cloudflare Pages

4 min read

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:

  1. Runs stedefast build (all 7 pipeline stages)
  2. Generates wrangler.toml from your cloudflare config
  3. Applies pending D1 migrations from registered modules
  4. 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.