Consolidated checklist for deploying the 8-phase billing hardening pass
(Phases 1–8 shipped on the billing-A-plus work). Go through top to
bottom; each step is either idempotent or clearly marked as one-time.
1. Migrations (apply in order)
All hand-written, idempotent (IF NOT EXISTS). Run against the shared
core Neon database via psql or the Neon SQL console:
psql "$DATABASE_URL" -f packages/core-database/drizzle/0013_credit_buckets.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0014_pending_upgrade_fields.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0015_rate_limit_buckets.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0016_plan_limits_tier_tightening.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0017_past_due_since.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0018_reconciliation_queue.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0019_billing_perf_indexes.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0020_signature_failures.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0021_integrity_reports.sql
psql "$DATABASE_URL" -f packages/core-database/drizzle/0022_billing_notifications.sqlNote: 0012_clever_titania.sql was auto-generated by drizzle-kit and
is tracked in _journal.json — it runs via pnpm migrate from the
core-database package.
2. Environment Variables
Set in Cloudflare Workers dashboard for logicspike-manager:
| Name | Required | Notes |
|---|---|---|
DATABASE_URL |
✅ | Existing |
RAZORPAY_KEY_ID |
✅ | Existing |
RAZORPAY_KEY_SECRET |
✅ | Existing |
RAZORPAY_WEBHOOK_SECRET |
✅ | Existing |
GATEWAY_SECRET |
✅ | Existing |
BILLING_ALERT_WEBHOOK_URL |
Optional | NEW — Slack Incoming Webhook URL for critical alerts. If unset, alerts fall back to structured error logs. |
3. Code Deploy
cd apps/manager
pnpm install # picks up new devDeps (vitest, pglite)
pnpm check-types
pnpm test # should be 161 green
pnpm deployAlso deploy packages consumers if you're bumping versions:
# only needed if core-billing or core-database changed (they did)
cd packages/core-billing && pnpm build # if there's a build step
cd packages/core-database && pnpm build4. Cloudflare Scheduled Workers (new crons to wire)
Add 5 scheduled triggers that POST to internal endpoints with
x-gateway-key: <GATEWAY_SECRET>:
| Trigger | Path | Frequency | Purpose |
|---|---|---|---|
reconcile |
POST /manager/billing/internal/reconcile |
*/5 * * * * (every 5 min) |
Retry flaky Razorpay calls |
escalation |
POST /manager/billing/internal/escalation |
0 * * * * (hourly) |
Past-due reminders + auto-downgrade |
addon_renewals |
POST /manager/billing/internal/addon-renewals |
0 * * * * (hourly) |
Debit recurring addons |
cleanup |
POST /manager/billing/internal/cleanup |
0 * * * * (hourly) |
Prune old rate-limit + reconciliation rows |
audit_integrity |
POST /manager/billing/internal/audit-integrity |
0 3 * * * (daily 3 AM UTC) |
Invariant audit |
Reference config (example wrangler.toml snippet for a companion
logicspike-billing-crons worker that fans out to manager):
[[triggers.crons]]
schedule = "*/5 * * * *"The cron handler itself is a short Worker that POSTs to the manager endpoints with the shared secret. No business logic lives there.
5. Alert Configuration
If you set BILLING_ALERT_WEBHOOK_URL, these events will page you:
Critical (immediate action):
- Reconciliation task abandoned after 10 retries
- Integrity audit found balance or ledger mismatch
- Signature-failure spike (10/hr per tenant on
/payment/verify) - Signature-failure spike (10/hr cumulative on
/webhook) - Auto-downgrade executed for a tenant
Warning:
- Integrity audit found stale pending upgrade (>72h)
Alerts write to the structured log regardless of webhook config.
6. Post-Deploy Verification
Run these queries 10 minutes after deploy to confirm health:
-- Migrations applied (should return 10 new tables)
SELECT table_name FROM information_schema.tables
WHERE table_name IN (
'billing_reconciliation_queue',
'billing_signature_failures',
'billing_integrity_reports',
'billing_notifications',
'rate_limit_buckets'
);
-- Credit bucket columns present on tenant_credits
SELECT column_name FROM information_schema.columns
WHERE table_name = 'tenant_credits';
-- should include: subscription_balance, subscription_expires_at, permanent_balance
-- Pending-upgrade columns on subscriptions
SELECT column_name FROM information_schema.columns
WHERE table_name = 'subscriptions'
AND column_name IN ('pending_razorpay_subscription_id',
'pending_billing_cycle', 'past_due_since');
-- First cron tick should fire within 10 min; check it happened
SELECT MAX(processed_at) FROM processed_payment_events;Then smoke-test from the dashboard:
- Buy a credit pack (test mode) →
GET /billing/credits/balanceshowsbuckets.permanentincremented. - Start a plan upgrade but close the modal → no state change in DB.
- Complete an upgrade → credits show up in
buckets.subscriptionwith anexpires_at30 days out.
7. Frontend Updates
Required for the full experience (see docs/billing/SYSTEM-REFERENCE.md
sections on pending-change):
Must-ship with backend (breaks UX otherwise):
- Widen
BillingCurrenttype inapps/seller-dashboard/src/hooks/use-billing.ts - Add "Complete payment" + "Cancel upgrade" banner driven by
billing.pendingChange.kind === "in_flight_upgrade" - Add "Cancel scheduled downgrade" banner driven by
billing.pendingChange.kind === "scheduled_downgrade" - Wire
POST /billing/pending-change/cancel - Handle
429responses globally withRetry-After
Should-ship within a week:
- Stage-based past-due alert styling (
alerts[].stage) - Credit bucket tooltip showing "X expiring on {date} + Y permanent"
When admin UI exists:
POST /billing/admin/adjust-creditsformGET /billing/admin/signature-failuresdashboard
8. Rollback Plan
Each migration is additive (new columns, new tables) — no drops, no
destructive UPDATEs. Rollback is code-only:
- Revert the manager deploy to the prior Worker version in the Cloudflare dashboard.
- The new tables + columns stay — they're NULL/DEFAULT on existing rows, don't affect old code.
- Disable the 5 new Scheduled Worker triggers.
No data loss. You can redeploy forward after fixing any issue.
9. Known Edge Cases
- Yearly plans dispense 12×
baseCreditsupfront (Phase 1.3). If your pricing page says "180k credits/year for Pro", yearly users now correctly get 180k at each annual renewal (not 15k). - Legacy
tenant_credits.balancecolumn is maintained by every write path as a mirror ofeffective_sub + permanent. Deprecated but still populated. Readers should prefer the bucket columns. /internal/credits/debitis deprecated — no service currently uses it. Services go throughCreditLedgerin@repo/core-billing.planFeaturestable is superseded byplanServiceLimits. Kept to avoid schema break; will be dropped in a future migration.
10. Testing Target
After deploy, expected test counts:
| Package | Tests |
|---|---|
apps/manager |
161 |
packages/core-billing |
40 |
| Total | 201 |
Run locally pre-deploy:
cd apps/manager && pnpm test
cd packages/core-billing && pnpm test