Skip to content

Cloudflare deployment ​

The portal deploys as one Worker with a D1 database, an R2 bucket, and static assets. All commands run from cloud/reform-assistant/. Wrangler comes from the devDependencies, so use npx wrangler ... (or the npm scripts) after npm ci.

What gets created ​

ResourceNameBinding in wrangler.jsonc
Workerreform-assistantmain: src/index.ts
D1 databasereform-assistantDB
R2 bucketreform-assistant-filesFILES
Static assets./publicASSETS
VarAPP_URL (https://reform-assistant.phucdoan.workers.dev in wrangler.jsonc; local wrangler dev overrides it from .dev.vars)plain variable

Current deployment ​

Deployed 2026-11-01 to the Cloudflare account used by the cf CLI login on the developer machine, on default Cloudflare addresses only (no custom domain yet).

WhatValue
Portal (Worker)https://reform-assistant.phucdoan.workers.dev
Docs (Pages project reform-assistant-docs)https://reform-assistant-docs.pages.dev
D1 databasereform-assistant, id 492e5526-7e6e-4278-a527-8f5850b43090, primary in western North America
R2 bucketreform-assistant-files (Cloudflare placed it in APAC despite the wnam hint; it only stores Stripe event archives)
Secrets setHERMES_LINK_TOKEN, STRIPE_WEBHOOK_SECRET (a temporary test secret so the E2E suite can sign events; replace with the real whsec_... once Stripe exists), STRIPE_PRICE_MONTHLY/QUARTERLY/ANNUAL (placeholders)
Not setSTRIPE_SECRET_KEY, so checkout and the billing portal answer 503 billing_not_configured

The secret values live only in Cloudflare and in ~/.config/reform-assistant/ on the developer machine (mode 0600).

How this deployment was made ​

The cf CLI (cf auth whoami) was already logged in with OAuth. cf d1 create, cf d1 migrations apply <id> --dir migrations, cf r2 buckets create and cf pages create created the resources. cf deploy delegates to Wrangler and guesses the project layout (it generated a cloudflare.config.ts pointing at the docs build), so the Worker itself was deployed with npx wrangler deploy using the cf login's OAuth token as CLOUDFLARE_API_TOKEN, and the docs with npx wrangler pages deploy docs/.vitepress/dist --project-name reform-assistant-docs --branch main.

Verification ​

npm run e2e with E2E_BASE_URL=https://reform-assistant.phucdoan.workers.dev passed 3 consecutive runs (15 pass, 0 fail), plus a browser check on the live site (signup, Secure; HttpOnly; SameSite=Lax cookie, inmate saved to D1, payments-off banner, 404). Test accounts were deleted from D1 afterwards.

Python clients need a User-Agent

Requests from Python's urllib with its default Python-urllib/3.x agent get 403 with Cloudflare error code: 1010 (browser-integrity check). The Hermes bridge sends User-Agent: reform-assistant-portal-sync/1.0; any other script calling the API must also set one.

1. Log in to Cloudflare ​

sh
npx wrangler login

2. Create the D1 database and paste its id ​

sh
npx wrangler d1 create reform-assistant

Wrangler prints the new database's database_id. Open wrangler.jsonc and replace the placeholder under d1_databases:

jsonc
"database_id": "00000000-0000-0000-0000-000000000000",

with the printed id. The all-zero value is only a placeholder; deploying with it fails. The database_name stays reform-assistant and migrations_dir stays migrations.

3. Create the R2 bucket ​

sh
npx wrangler r2 bucket create reform-assistant-files

The bucket holds one object per verified Stripe webhook event, at stripe/events/<event_id>.json, and one record per duplicate subscription the webhook handler detected, at stripe/duplicates/<subscription_id>.json.

4. Apply the schema to the remote database ​

sh
npx wrangler d1 migrations apply reform-assistant --remote

Run this again after every new file in migrations/. (npm run db:migrate:local is the local-only variant.)

5. Set the secrets ​

Each secret is set with wrangler secret put, which prompts for the value. Secrets are never stored in the repository.

sh
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
npx wrangler secret put STRIPE_PRICE_MONTHLY
npx wrangler secret put STRIPE_PRICE_QUARTERLY
npx wrangler secret put STRIPE_PRICE_ANNUAL
npx wrangler secret put HERMES_LINK_TOKEN
SecretWhere the value comes from
STRIPE_SECRET_KEYStripe Dashboard, Developers, API keys (use the test key sk_test_... for the sandbox)
STRIPE_WEBHOOK_SECRETSigning secret (whsec_...) of the Dashboard webhook endpoint that points at the deployed Worker; see Stripe setup
STRIPE_PRICE_MONTHLY, STRIPE_PRICE_QUARTERLY, STRIPE_PRICE_ANNUALThe three price_... ids from the same Stripe mode as the key
HERMES_LINK_TOKENGenerate a long random value, for example openssl rand -hex 32, and store the same value in the Hermes token file; see Hermes bridge

Until the Stripe secrets are set, billing endpoints answer 503 billing_not_configured while the rest of the portal works. Until HERMES_LINK_TOKEN is set, every /api/hermes/* request is rejected with 401. Set STRIPE_SECRET_KEY in every deployed environment that receives webhooks: besides billing calls it is what lets the webhook handler re-fetch the current subscription from Stripe, so out-of-order events cannot write stale state (see Stripe webhooks).

6. Set APP_URL ​

APP_URL is a plain variable under vars in wrangler.jsonc. The Worker uses it for the Stripe success, cancel, and portal return URLs; it does not decide the cookie's Secure flag, which follows the scheme of each request (HTTPS in production). Before the first deploy, set it to the HTTPS origin the portal will be reached on, or Stripe will send families back to http://localhost:8787. Until a custom domain exists, that is the https://reform-assistant.<your-subdomain>.workers.dev URL Cloudflare assigns to the Worker.

7. Deploy ​

sh
npm run deploy

This runs wrangler deploy, which uploads the Worker and everything in public/. Check it:

sh
curl -s https://<your-origin>/api/plans

The response is the JSON list of the three plans. Then add the Stripe webhook endpoint for https://<your-origin>/api/stripe/webhook as described in Stripe setup, and point the Hermes host at the same origin with REFORM_PORTAL_URL.

Adding the custom domain later ​

After the domain's DNS zone is on the Cloudflare account (for example after a registrar transfer):

  1. Attach the domain to the Worker, either in the Dashboard (Workers & Pages, the reform-assistant Worker, Settings, Domains & Routes, Add, Custom Domain) or in wrangler.jsonc:

    jsonc
    "routes": [{ "pattern": "portal.example.com", "custom_domain": true }]
  2. Change APP_URL in wrangler.jsonc to https://portal.example.com and redeploy with npm run deploy.

  3. Edit the Stripe webhook endpoint URL to https://portal.example.com/api/stripe/webhook.

  4. Change REFORM_PORTAL_URL for the Hermes cron job to the new origin.

Session cookies are host-scoped (no Domain attribute), so existing users are asked to log in again on the new host.

Rollback ​

List recent deployments and roll back to a previous one:

sh
npx wrangler deployments list
npx wrangler rollback            # previous version, with a confirmation prompt
npx wrangler rollback <version-id>

A rollback only swaps the Worker code and assets. It does not undo D1 migrations or data changes. To recover the database, use D1 Time Travel:

sh
npx wrangler d1 time-travel info reform-assistant
npx wrangler d1 time-travel restore reform-assistant --timestamp=<RFC3339 timestamp>

Migrations are plain forward-only SQL files in this repository, so write new migrations to stay compatible with the previous Worker version if you may need to roll back code.