Skip to content

Deployment

Hosting is $0: Cloudflare Pages (free, commercial use allowed) + Turso free tier + Resend free tier.

Terminal window
npm run build

Output lands in dist/. A green build is your deploy gate — never deploy red.

Terminal window
npx wrangler pages deploy dist --project-name=your-project

In the Cloudflare dashboard (or .dev.vars locally — never commit secrets):

  • TURSO_DATABASE_URL — your hosted libsql:// URL
  • TURSO_AUTH_TOKEN
  • DODO_PAYMENTS_API_KEY — live key
  • DODO_PAYMENTS_WEBHOOK_SECRET
  • DODO_PRODUCTS_JSON — your tier catalog (see Payments)
  • RESEND_API_KEY, EMAIL_FROM
  • BETTER_AUTH_SECRET, BETTER_AUTH_URL (your live URL)
  • SITE_URL (your live URL)

In Dodo/Stripe/Lemon Squeezy, set the webhook URL to https://yourdomain.com/api/webhook. Test with a real $1 checkout, then delete the test row.

On Cloudflare Pages, functions/api/webhook.ts is the receiver. A Pages deploy serves dist/client as static assets; it never runs the Astro server route at src/pages/api/webhook.ts. Without the Pages Function the webhook has nowhere to land, so no orders or licenses row is written even though checkout keeps working. The same applies to license verification at functions/api/license/verify.ts.

The Pages Functions read their configuration from Pages secrets and vars, not .env: set DODO_PAYMENTS_WEBHOOK_SECRET, DODO_PRODUCTS_JSON, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, RESEND_API_KEY, and EMAIL_FROM with wrangler pages secret put <NAME> --project-name <project>.

  • npm run build green
  • Secrets set in Cloudflare, none committed
  • Webhook endpoint registered with your provider
  • One live end-to-end purchase verified