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 rehearsal

Checking command-center API...

Do not paste Stripe keys, webhook secrets, license signing secrets, API tokens, or customer data into dashboard files.

Cloud Billing Snapshot

Status Cloudflare Worker, D1 billing storage, device pairing, and recovery runtime configuration are deployed and health verified; email recovery stays fail-closed until sender-domain onboarding and a real delivery roundtrip pass, and paid launch remains gated by guarded live-mode preflight, legal review. Ready
Worker URL https://billing.porterlabz.com Ready
D1 Database ID 148a0520-51bb-4f31-895b-02a99efd31b4 Ready
Secret Status Deployed Worker secret configuration verified Ready
Config Sync Billing Worker Config Sync Applied Ready
Public Checkout Disabled - fail closed Ready
Account Recovery Disabled - fail closed Ready
Recovery Prerequisites 4/4 runtime pieces ready Ready
Recovery Sender Domain Onboarding required Ready
Recovery Delivery Blocked - sending domain not onboarded Ready
Check npm run cloud:billing:check Ready
Readiness npm run test:cloud-billing-worker Ready

Billing Worker Config Sync

npm run cloud:billing:config:sync

This 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.

Status Billing Worker Config Sync Applied Ready
Applied 1 Ready
Waiting 0 Ready
Invalid 0 Ready

Applied

  • Billing Worker health verification time: 2026-07-18T13:46:56.839Z

Waiting

  • No non-secret billing config handoffs are waiting.

Needs Fix

Routes

RouteStatus
GET /healthReady
POST /api/billing/checkoutReady
POST /api/billing/portalReady
POST /api/billing/webhookReady
GET /api/license/statusReady
POST /api/license/pairing-codeReady
POST /api/license/pairReady
POST /api/account/recovery/startReady
POST /api/account/recovery/completeReady

Secrets

SecretWhere
STRIPE_SECRET_KEYSecret store
STRIPE_WEBHOOK_SECRETSecret store
NYRA_STRIPE_PRICE_PRO_MONTHLYSecret store
NYRA_BILLING_SUCCESS_URLSecret store
NYRA_BILLING_CANCEL_URLSecret store
NYRA_BILLING_RETURN_URLSecret store
NYRA_LICENSE_API_TOKENSecret store
NYRA_LICENSE_SIGNING_SECRETSecret store

Live Blockers

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:

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

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

Required Cloudflare Secrets

Use wrangler secret put or Cloudflare dashboard secrets:

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:

Rollback