Payments Cloud
Cloud Billing Worker
Cloudflare Worker + D1 backend for Stripe Checkout, Customer Portal, webhooks, entitlement state, device activation, pairing, and account recovery.
Cloud Billing Form
Billing rehearsalChecking command-center API...
Do not paste Stripe keys, webhook secrets, license signing secrets, API tokens, or customer data into dashboard files.
Cloud Billing Snapshot
Billing Worker Config Sync
npm run cloud:billing:config:syncThis turns Cloud Billing page answers into non-secret production Worker config, including ALLOWED_ORIGIN, D1 database_id, and Stripe API version. Stripe keys, webhook secrets, license secrets, and customer data stay out of repo files.
Applied
- Billing Worker health verification time: 2026-07-18T13:46:56.839Z
Waiting
- No non-secret billing config handoffs are waiting.
Needs Fix
- No invalid billing config values found.
Routes
| Route | Status |
|---|---|
| GET /health | Ready |
| POST /api/billing/checkout | Ready |
| POST /api/billing/portal | Ready |
| POST /api/billing/webhook | Ready |
| GET /api/license/status | Ready |
| POST /api/license/pairing-code | Ready |
| POST /api/license/pair | Ready |
| POST /api/account/recovery/start | Ready |
| POST /api/account/recovery/complete | Ready |
Secrets
| Secret | Where |
|---|---|
STRIPE_SECRET_KEY | Secret store |
STRIPE_WEBHOOK_SECRET | Secret store |
NYRA_STRIPE_PRICE_PRO_MONTHLY | Secret store |
NYRA_BILLING_SUCCESS_URL | Secret store |
NYRA_BILLING_CANCEL_URL | Secret store |
NYRA_BILLING_RETURN_URL | Secret store |
NYRA_LICENSE_API_TOKEN | Secret store |
NYRA_LICENSE_SIGNING_SECRET | Secret store |
Live Blockers
- guarded live-mode preflight
- legal review
Deployment Manifest
{
"service": "nyra-billing-api",
"scope": "Cloudflare Worker and D1 billing backend for NyrA Swarm Little Buddy paid beta",
"runtime": "Cloudflare Workers",
"storage": {
"type": "Cloudflare D1",
"binding": "DB",
"databaseName": "nyra_billing",
"migrationsDir": "cloud/billing-worker/migrations",
"tables": [
"billing_customers",
"processed_events",
"device_activations",
"device_pairing_codes",
"account_recovery_challenges"
]
},
"routes": [
"GET /health",
"POST /api/billing/checkout",
"POST /api/billing/portal",
"POST /api/billing/webhook",
"GET /api/license/status",
"POST /api/license/pairing-code",
"POST /api/license/pair",
"POST /api/account/recovery/start",
"POST /api/account/recovery/complete"
],
"requiredSecrets": [
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"NYRA_STRIPE_PRICE_PRO_MONTHLY",
"NYRA_BILLING_SUCCESS_URL",
"NYRA_BILLING_CANCEL_URL",
"NYRA_BILLING_RETURN_URL",
"NYRA_LICENSE_API_TOKEN",
"NYRA_LICENSE_SIGNING_SECRET"
],
"requiredVars": [
"ALLOWED_ORIGIN",
"STRIPE_API_VERSION",
"NYRA_PUBLIC_CHECKOUT_ENABLED",
"NYRA_ACCOUNT_RECOVERY_ENABLED"
],
"optionalRecovery": {
"featureFlag": "NYRA_ACCOUNT_RECOVERY_ENABLED",
"secret": "NYRA_ACCOUNT_RECOVERY_SECRET",
"fromEmailVar": "NYRA_ACCOUNT_RECOVERY_FROM_EMAIL",
"emailBinding": "EMAIL",
"allowedSender": "accounts@porterlabz.com",
"domain": "porterlabz.com",
"defaultState": "disabled"
},
"checks": [
"npm run cloud:billing:check",
"npm run cloud:billing:migrate",
"npm run test:cloud-billing-worker"
],
"liveBlockers": [
"Cloudflare account and D1 database ID",
"Billing Worker custom domain",
"Stripe test/live secrets in Wrangler secret store",
"Stripe product/price after final monthly price",
"Customer Portal configuration",
"Stripe webhook endpoint and signing secret",
"Cloudflare Email Service sender-domain authorization for optional account recovery",
"Support inbox roundtrip",
"Attorney/accountant review"
]
}Wrangler Config
{
"$schema": "../../node_modules/wrangler/config-schema.json",
"name": "nyra-billing-api",
"main": "src/index.mjs",
"compatibility_date": "2026-07-15",
"compatibility_flags": [
"nodejs_compat"
],
"observability": {
"enabled": true
},
"vars": {
"ALLOWED_ORIGIN": "http://localhost:5173",
"STRIPE_API_VERSION": "2026-06-24.dahlia",
"NYRA_PUBLIC_CHECKOUT_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_FROM_EMAIL": "accounts@porterlabz.com"
},
"send_email": [
{
"name": "EMAIL",
"allowed_sender_addresses": [
"accounts@porterlabz.com"
]
}
],
"d1_databases": [
{
"binding": "DB",
"database_name": "nyra_billing",
"database_id": "replace-with-cloudflare-d1-database-id",
"migrations_dir": "migrations"
}
],
"env": {
"local": {
"vars": {
"ALLOWED_ORIGIN": "http://localhost:5173",
"STRIPE_API_VERSION": "2026-06-24.dahlia",
"NYRA_PUBLIC_CHECKOUT_ENABLED": "true",
"NYRA_ACCOUNT_RECOVERY_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_FROM_EMAIL": "accounts@porterlabz.com"
},
"send_email": [
{
"name": "EMAIL",
"allowed_sender_addresses": [
"accounts@porterlabz.com"
]
}
],
"d1_databases": [
{
"binding": "DB",
"database_name": "nyra_billing",
"database_id": "replace-with-cloudflare-d1-database-id",
"migrations_dir": "migrations"
}
]
},
"staging": {
"workers_dev": true,
"vars": {
"ALLOWED_ORIGIN": "http://localhost:5173",
"STRIPE_API_VERSION": "2026-06-24.dahlia",
"NYRA_PUBLIC_CHECKOUT_ENABLED": "true",
"NYRA_ACCOUNT_RECOVERY_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_FROM_EMAIL": "accounts@porterlabz.com"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "nyra_billing_test",
"database_id": "8b60fd57-79e8-4be3-aeab-5b6023ddf65f",
"migrations_dir": "migrations"
}
]
},
"production": {
"routes": [
{
"pattern": "billing.porterlabz.com",
"custom_domain": true
}
],
"vars": {
"ALLOWED_ORIGIN": "https://porterlabz.com",
"STRIPE_API_VERSION": "2026-06-24.dahlia",
"NYRA_PUBLIC_CHECKOUT_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_ENABLED": "false",
"NYRA_ACCOUNT_RECOVERY_FROM_EMAIL": "accounts@porterlabz.com"
},
"send_email": [
{
"name": "EMAIL",
"allowed_sender_addresses": [
"accounts@porterlabz.com"
]
}
],
"d1_databases": [
{
"binding": "DB",
"database_name": "nyra_billing",
"database_id": "148a0520-51bb-4f31-895b-02a99efd31b4",
"migrations_dir": "migrations"
}
]
}
}
}
D1 Migrations
-- 0001_billing.sql
CREATE TABLE IF NOT EXISTS billing_customers (
customer_id TEXT PRIMARY KEY,
checkout_session_id TEXT,
subscription_id TEXT,
device_id TEXT,
email TEXT,
status TEXT,
entitlement TEXT,
price_id TEXT,
current_period_end INTEGER,
cancel_at_period_end INTEGER NOT NULL DEFAULT 0,
last_invoice_id TEXT,
last_invoice_status TEXT,
stripe_entitlements_json TEXT,
updated_at TEXT NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS idx_billing_customers_checkout_device
ON billing_customers (checkout_session_id, device_id);
CREATE INDEX IF NOT EXISTS idx_billing_customers_subscription
ON billing_customers (subscription_id);
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
processed_at TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS device_activations (
customer_id TEXT NOT NULL,
device_id TEXT NOT NULL,
activated_at TEXT NOT NULL,
last_checked_at TEXT NOT NULL,
last_active INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (customer_id, device_id)
) STRICT;
-- 0002_device_pairing.sql
CREATE TABLE IF NOT EXISTS device_pairing_codes (
code_hash TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
source_device_id TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at INTEGER NOT NULL,
claimed_at TEXT,
claimed_device_id TEXT
) STRICT;
CREATE INDEX IF NOT EXISTS idx_device_pairing_customer
ON device_pairing_codes (customer_id, expires_at);
-- 0003_account_recovery.sql
CREATE INDEX IF NOT EXISTS idx_billing_customers_normalized_email
ON billing_customers (lower(trim(email)));
CREATE TABLE IF NOT EXISTS account_recovery_challenges (
challenge_id TEXT PRIMARY KEY,
customer_id TEXT,
device_id TEXT NOT NULL,
email_hash TEXT NOT NULL,
code_hash TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
attempt_count INTEGER NOT NULL DEFAULT 0,
consumed_at TEXT,
delivery_status TEXT NOT NULL DEFAULT 'pending',
last_attempt_at TEXT
) STRICT;
CREATE INDEX IF NOT EXISTS idx_account_recovery_email_created
ON account_recovery_challenges (email_hash, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_account_recovery_device_created
ON account_recovery_challenges (device_id, created_at DESC);NyrA Cloud Billing Worker
This Worker is the Cloudflare-native billing backend path for NyrA Swarm Little Buddy. It mirrors the local Node billing service routes while replacing file-backed entitlement storage with D1.
It does not contain live Stripe keys, webhook secrets, certificate material, or customer data.
Routes
| Route | Purpose |
|---|---|
GET /health |
Verifies required secrets and D1 reachability. |
POST /api/billing/checkout |
Creates a Stripe Checkout Session in subscription mode. |
POST /api/billing/portal |
Creates a Stripe Customer Portal Session for an authenticated customer/device. |
POST /api/billing/webhook |
Verifies Stripe webhook signatures and records subscription/customer state. |
GET /api/license/status |
Returns active entitlement, offline grace metadata, device activation, license blob, and device access token. |
POST /api/license/pairing-code |
Creates a ten-minute, single-use device pairing code from an authenticated paid device. |
POST /api/license/pair |
Exchanges a pairing code for a new device-scoped token after rechecking the subscription. |
POST /api/account/recovery/start |
Starts a non-enumerating, email-delivered device recovery challenge when recovery is fully configured. |
POST /api/account/recovery/complete |
Consumes a single-use recovery code and issues a device token after rechecking entitlement. |
Device access tokens are signed bearer tokens for the paid device and include expiresAt. They expire at the subscription offline-grace boundary when Stripe period data is available, or after a 24-hour fallback window while checkout/subscription state is still settling.
Storage
D1 tables are defined in migrations/0001_billing.sql:
billing_customersprocessed_eventsdevice_activationsdevice_pairing_codesfrommigrations/0002_device_pairing.sqlaccount_recovery_challengesplus the normalized-email index frommigrations/0003_account_recovery.sql
Pairing codes are 12 high-entropy characters, expire after ten minutes, and are single use. D1 stores only a SHA-256 hash of each code. Creating a new code invalidates the previous unused code for that customer. Pairing and recovery completion require the device's normalized expectedEmail; the Worker verifies it against the paid customer before consuming the one-time credential.
Account recovery is separately fail-closed. The Worker config declares an EMAIL send binding restricted to accounts@porterlabz.com, but production keeps NYRA_ACCOUNT_RECOVERY_ENABLED=false until porterlabz.com is onboarded to Cloudflare Email Service, NYRA_ACCOUNT_RECOVERY_SECRET is set, and a real recovery roundtrip passes. Recovery responses do not reveal whether an email is registered; D1 stores peppered hashes instead of the raw email or code. The client hides email recovery when /health reports that any required recovery capability is unavailable.
Production checkout is fail-closed behind the non-secret NYRA_PUBLIC_CHECKOUT_ENABLED variable. Keep it false until every live-money gate is cleared; local Stripe test-mode rehearsal may set it to true.
This path removes the paid-beta need for NYRA_ENTITLEMENTS_FILE when the Worker is selected as the hosted backend, but it still requires a real Cloudflare account, D1 database, custom domain, Stripe secrets, webhook endpoint, and support/legal readiness before live payments.
Required Secrets
Set these with wrangler secret put or the Cloudflare dashboard secret store. Do not commit them.
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
NYRA_STRIPE_PRICE_PRO_MONTHLY
NYRA_STRIPE_PORTAL_CONFIGURATION_ID (optional; use an explicit bpc_ ID for staging)
NYRA_BILLING_SUCCESS_URL
NYRA_BILLING_CANCEL_URL
NYRA_BILLING_RETURN_URL
NYRA_LICENSE_API_TOKEN
NYRA_LICENSE_SIGNING_SECRET
Account recovery additionally requires NYRA_ACCOUNT_RECOVERY_SECRET in the secret store, NYRA_ACCOUNT_RECOVERY_FROM_EMAIL as a non-secret Worker variable, and an authorized Cloudflare Email Service EMAIL binding. These are not launch prerequisites while NYRA_ACCOUNT_RECOVERY_ENABLED=false; device pairing remains the returning-device path.
Before enabling recovery, verify the sender domain with the current Wrangler authentication:
npx wrangler email sending list
If porterlabz.com is not listed, onboard it through Cloudflare Email Service before deploying an enabled recovery flow. The scoped Cloudflare credential used by automation must include the Email Sending permissions required by Wrangler; an unrelated Worker token is not enough.
Local Checks
npm run cloud:billing:check
npm run cloud:billing:migrate
npm run test:cloud-billing-worker
Deploy Rehearsal
npx wrangler d1 create nyra_billing
npx wrangler d1 migrations apply nyra_billing --cwd cloud/billing-worker --env local --local
npx wrangler dev --cwd cloud/billing-worker --env local
For hosted test-mode proof, use the isolated staging environment. It has its own
nyra_billing_test D1 binding, a workers.dev URL, test-only Wrangler secrets,
and public checkout enabled only for Stripe test mode. Never point this environment
at nyra_billing or a live Stripe key.
npm run cloud:billing:staging:migrate
npm run cloud:billing:staging:deploy
The staging Checkout success, cancel, and Portal return URLs are served directly by
the Worker at /billing/success, /billing/cancel, and /account. Stripe secret or
restricted keys stay in Wrangler secrets; restricted rk_test_ keys are supported.
For production, replace the D1 database ID in wrangler.jsonc, set secrets outside the repo, configure the Stripe webhook endpoint to https://<billing-domain>/api/billing/webhook, and use Stripe test mode first. npm run cloud:billing:deploy applies remote D1 migrations before uploading the Worker.
After saving the Cloud Billing dashboard fields, run:
npm run cloud:billing:config:sync
npm run test:cloud-billing-config-sync
The sync writes only non-secret production Worker config values such as ALLOWED_ORIGIN, D1 database_id, and the pinned Stripe API version. Stripe keys, webhook secrets, license secrets, and customer data must stay in Wrangler secrets, Stripe, or temporary shell variables.
No-Go Rules
- Do not run live mode until the command center has a monthly price, hosted AI cap, confirmed support inbox, domain/backend URL, Customer Portal setup, attorney/accountant review, and signed/store-trusted release path.
- Do not put Stripe secrets, webhook secrets, license signing secrets, API tokens, or customer records in the repo or dashboard JSON.
- Do not issue permanent device access tokens. Paid device tokens must expire and refresh through
GET /api/license/status. - Do not expose checkout publicly until
deployability.statusis no longerNO_GO_LIVE_MONEY. - Keep production
NYRA_PUBLIC_CHECKOUT_ENABLED=falseuntil the final money preflight is green. - Keep production
NYRA_ACCOUNT_RECOVERY_ENABLED=falseuntil the sender domain and Worker email binding are authorized and a real inbox roundtrip passes.
Cloud Billing Worker Deployment Checklist
This checklist is for the Cloudflare Worker + D1 hosted billing path. It is not legal, tax, accounting, or Stripe support advice.
Before Deployment
- Cloudflare account exists.
- Billing Worker custom domain or workers.dev URL is approved.
- D1 database
nyra_billingis created. - Cloud Billing dashboard fields are saved, then
npm run cloud:billing:config:synchas updated non-secret Worker config. cloud/billing-worker/wrangler.jsonchas the real D1 database ID and productionALLOWED_ORIGIN.- Migrations are applied.
- Production
NYRA_PUBLIC_CHECKOUT_ENABLEDremainsfalseuntil the final money preflight clears. - Production
NYRA_ACCOUNT_RECOVERY_ENABLEDremainsfalseuntil Cloudflare Email Service authorizes the sender and a real recovery roundtrip passes. porterlabz.comappears innpx wrangler email sending list, and the WorkerEMAILbinding is restricted toaccounts@porterlabz.com.- Stripe product and monthly Price ID exist after the final dashboard price is chosen.
- Customer Portal cancellation, invoices, and payment method update settings are configured.
- Support inbox is created, secured, and tested.
- Attorney/accountant review is approved for paid beta.
Required Cloudflare Secrets
Use wrangler secret put or Cloudflare dashboard secrets:
STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETNYRA_STRIPE_PRICE_PRO_MONTHLYNYRA_BILLING_SUCCESS_URLNYRA_BILLING_CANCEL_URLNYRA_BILLING_RETURN_URLNYRA_LICENSE_API_TOKENNYRA_LICENSE_SIGNING_SECRET
Optional email recovery also requires NYRA_ACCOUNT_RECOVERY_SECRET, NYRA_ACCOUNT_RECOVERY_FROM_EMAIL, and an authorized Cloudflare Email Service EMAIL binding. Do not enable the feature when any one is missing.
Required Verification
Run locally:
npm run cloud:billing:check
npm run cloud:billing:migrate
npm run cloud:billing:config:sync
npm run test:cloud-billing-config-sync
npm run test:cloud-billing-worker
npm run billing:live-preflight -- --mode test --allow-blocked
Then, against the hosted Worker in Stripe test mode:
GET /healthreportsok: true.- Checkout Session creation returns a Stripe-hosted Checkout URL.
- Stripe webhook
checkout.session.completedrecords the customer and device metadata. - Stripe webhook
invoice.paidrecords active subscription state. - Stripe Customer Portal creation works with a device access token.
- Desktop
Refresh Activationreturns active license state after checkout/webhook sync. - License status returns
deviceAccessTokenExpiresAtIso, and the signed device access token contains the sameexpiresAtvalue. - An authenticated paid device can create a pairing code; a second device can claim it once; replay and expiry are rejected; D1 never stores the raw code.
- With recovery deliberately enabled in a non-production rehearsal, known and unknown emails return indistinguishable start responses, only an active known account receives mail, wrong attempts are capped, a valid code activates only the requesting device, and replay fails.
- The hosted recovery email is delivered from
accounts@porterlabz.com, the app completes activation on a clean second device, and the sender credential or binding is never exposed to the client. - Duplicate webhook delivery is idempotent.
- Bad webhook signature is rejected.
Rollback
- Disable public checkout links first.
- Set
NYRA_PUBLIC_CHECKOUT_ENABLED=falsebefore rollback work begins. - Pause the Stripe Price or stop sending customers to Checkout if activation breaks.
- Roll back the Worker deployment through Cloudflare.
- Preserve D1 data before destructive changes.
- Route affected customers through the support runbook.