Billing
Stripe Integration — Implementation Roadmap
24 Linear tickets  ·  2 sprints  ·  ~9 days with Claude Code
────────────────────────────────
Tyler + Tristan  ·  May 5–16, 2026
Pricing Tiers

Four tiers. Stripe only enters the picture at upgrade.

Feature Free Team · $20/seat/mo Business · $40/seat/mo Enterprise · Custom
Annual price$192/seat/yr (20% off)$384/seat/yr (20% off)Negotiated
Stripe requiredNoYesYesNo — off Stripe
Fixes per 30 days5UnlimitedUnlimitedUnlimited
Doc runs per 30 days1UnlimitedUnlimitedUnlimited
Linear cycles per 30 days5UnlimitedUnlimitedUnlimited
Connected reposUnlimitedUp to 5UnlimitedUnlimited
All 7 workflowsLimitedFull accessFull accessFull access
Jira integrationNoNoYesYes
Audit logsNoNoYesYes
Usage reportingNoNoYesYes
Priority fix queueNoNoYesYes
SSO / SAMLNoNoNoYes
Full RBACNoNoNoYes
VPC / complianceNoNoNoYes
Overage behaviorHard stopNo limitsNo limitsNo limits
Free tier resetRolling 30d per tenantN/AN/AN/A

Free tier counters are enforced entirely in Hydra's DB. No Stripe record exists for a free tenant. Enterprise is handled outside Stripe entirely.

Annual vs Monthly Billing

4 Stripe Price objects. Same products, different intervals.

The 4 Price Objects (one-time Stripe setup)

Price ID env varAmountIntervalPer
STRIPE_TEAM_MONTHLY_PRICE_ID$20.00monthseat
STRIPE_TEAM_ANNUAL_PRICE_ID$192.00yearseat
STRIPE_BUSINESS_MONTHLY_PRICE_ID$40.00monthseat
STRIPE_BUSINESS_ANNUAL_PRICE_ID$384.00yearseat

Annual savings

20%
discount on annual vs monthly × 12
$48
saved per Team seat per year
$96
saved per Business seat per year

How this works in Stripe

Monthly plan
Invoice every 30 days

Stripe creates an invoice each month. Customer is charged monthly. Proration on mid-cycle changes is bundled into the next monthly invoice.

Important: no mid-period interval switching
Monthly ↔ Annual requires subscription schedules

Switching interval (month → year or year → month) mid-period is not supported natively. Requires Stripe Subscription Schedules — a future edge case. In v1: customers on monthly must cancel and re-subscribe to switch to annual. Annual customers cannot switch to monthly mid-year.

Frontend toggle behavior

The monthly/annual toggle on the billing page changes the price_id sent to POST /api/billing/checkout. Nothing else changes. Stripe handles the rest.

Invoices

Stripe creates an invoice for every billing event. You never create invoices — you handle them.

Invoice lifecycle

1
Stripe creates invoice automatically
On subscription creation, on each renewal, and on mid-cycle changes (prorations). You do not call the invoice API to create recurring invoices.
2
Stripe attempts to collect payment
Uses the customer's default payment method. Stripe Smart Retries will retry on soft declines before firing invoice.payment_failed.
3a
Payment succeeds → invoice.paid
This is the canonical provisioning signal. Set plan_tier, stripe_subscription_status=active, stripe_current_period_end, stripe_seat_count in DB.
3b
Payment fails → invoice.payment_failed
Set stripe_subscription_status=past_due. Start 7-day grace period. Do not immediately revoke access.
3c
3DS required → invoice.payment_action_required
EU / SCA cards require customer to authenticate. Send email with Stripe-hosted authentication link. Subscription stays incomplete until authenticated.

Invoice preview — used for proration

What customers see

Stripe sends receipt emails automatically

Configure in Dashboard: Settings → Business settings → Customer emails. Enable "Successful payments" and "Failed payments." Stripe handles the email — you do not build invoice emails.

Invoice history in Customer Portal

Customers can view and download all past invoices from the Customer Portal without contacting you. Enable under Portal configuration → Invoice history.

Annual plan invoicing

Annual customers receive one large invoice per year (e.g. $192 × 10 seats = $1,920). Mid-year seat additions generate a separate prorated invoice for the remainder of the year.

Architecture Overview

One Stripe Customer + one Subscription per paid tenant. 8 webhook events.

Stripe objects per tenant

ObjectCreated whenKey fields
Customer cus_First upgrade attemptemail, name, metadata: hydra_tenant_id
Subscription sub_Checkout completesitems[0].price_id, items[0].quantity
SubscriptionItem si_With subscriptionid required for seat/plan updates
InvoiceEvery billing cyclestatus, amount_due, period_start/end
PaymentIntentInside invoicestatus, last_payment_error

DB state (tenants table)

ColumnSet by
plan_tierinvoice.paid handler
stripe_subscription_statusAll subscription events
stripe_seat_countinvoice.paid, sub.updated
stripe_current_period_endinvoice.paid
stripe_customer_idCheckout session creation

8 webhook events — all required

EventAction
invoice.paidProvision access — canonical signal
invoice.payment_failedSet past_due, start grace period
invoice.payment_action_required3DS — send auth email to customer
checkout.session.completedStore subscription_id, await invoice
customer.subscription.createdIf active: provision; if incomplete: hold
customer.subscription.updatedHandle seat, status, price changes
customer.subscription.deletedSchedule revocation at period_end
entitlements.active_entitlement_summary.updatedSync feature flags from Stripe
This is not a normal feature sprint.

Stripe retries failed webhooks
for 72 hours.
Every handler fires more than once.

Idempotency is not optional. A handler that runs twice must produce the same result. A missed event = customer paid, no access. A double-processed event = double charge. Every handler checks current state before writing.

All 24 Linear Tickets

Full scope. Ordered by dependency.

Sprint 1 — May 5–9  14 tickets · ~5.5d

TicketTitleEstPriority
BILL-01Stripe account + Products + Prices + Portal config0.5dUrgent
BILL-02DB migration — billing columns on tenants2hUrgent
BILL-03Free tier enforcement middleware0.5dUrgent
BILL-04Free tier reset job (rolling 30-day per tenant)2hHigh
BILL-07Webhook endpoint + signature verification2hUrgent
BILL-05Checkout session endpoint0.5dUrgent
BILL-06Post-checkout success page + session retrieval2hHigh
BILL-08invoice.paid handler — provision paid access2hUrgent
BILL-09invoice.payment_failed + grace period2hHigh
BILL-12checkout.session.completed + subscription.created2hHigh
BILL-10customer.subscription.updated handler0.5dUrgent
BILL-11customer.subscription.deleted handler2hHigh
BILL-13Seat update — proration preview + apply0.5dHigh
BILL-14Plan upgrade endpoint (Team → Business)2hHigh

Sprint 2 — May 12–16  10 tickets · ~4d

TicketTitleEstPriority
BILL-15Customer Portal session endpoint1hHigh
BILL-16Access gating middleware0.5dUrgent
BILL-17Billing settings API0.5dHigh
BILL-22Free tier enforcement unit tests2hHigh
BILL-18Frontend — upgrade prompt components0.5dHigh
BILL-19Frontend — billing/upgrade page0.75dHigh
BILL-20Frontend — billing settings page0.5dHigh
BILL-21Frontend — post-checkout success page2hMedium
BILL-23Stripe test mode — integration tests1dHigh
BILL-24Webhook handler tests via Stripe CLI1dHigh
~9d
with Claude Code
~18.5d
solo engineering
24
total Linear tickets
Phase 0 — Before Day 1

Six decisions block Day 1. No code until these are resolved.

Tyler's calls — all 6 are blockers

DecisionDefault
Seat definitionDeveloper with active connected repo
Billing cycle anchorPer-customer signup date
Proration in portalcreate_prorations
billing_modeclassic (Stripe default)
Entitlements APIEnable it
Grace period on failed payment7 days

Defaults are safe to ship with. If Tyler doesn't decide, use the defaults — don't block coding on decisions that can be changed later.

BILL-01: Stripe setup checklist

1
Create separate Hydra Stripe account
Not Iru's account.
2
Create Team + Business Products
Two products: "Hydra Team" and "Hydra Business"
3
Create 4 Price objects
Team mo $20 · Team yr $192 · Business mo $40 · Business yr $384
4
Configure Customer Portal
Enable: seat quantity changes, plan switches, cancellation, invoice history
5
Register webhook endpoint
https://api.hydra.iru.dev/api/webhooks/stripe — all 8 events. Copy signing secret to env.
6
Install Stripe CLI
brew install stripe/stripe-cli/stripe && stripe login
Sprint 1 · May 5–9 · 14 tickets

Foundation.
Free Tier.
Webhooks.

Full backend complete by Friday. All 8 webhook events handled. Free tier blocks. Upgrade flow works end-to-end in test mode.
Day 1 — Mon May 5

Foundation. Every other card depends on this day.

Goal: Stripe account live in test mode. DB schema extended with all 11 billing columns. All other cards unblocked.
BILL-01 Stripe account + Products + Prices + Portal config
Urgent 0.5d — ops, not code billing infrastructure
Tyler drives. Create separate Hydra Stripe account. Create Team and Business Products. Create all 4 Price objects at correct amounts. Configure BillingPortal.Configuration (seat quantity, plan switches, cancellation, invoice history). Register webhook endpoint and copy signing secret. Store all Price IDs and Portal Config ID as env vars.
Blocks: everything Stripe-dependent
BILL-02 DB migration — billing columns on tenants table
Urgent 2h with Claude Code billing database
Add 11 columns: free_tier_fixes_used, free_tier_doc_runs_used, free_tier_cycles_used, free_tier_reset_at, stripe_customer_id, stripe_subscription_id, stripe_subscription_status, stripe_price_id, stripe_current_period_end, stripe_seat_count, plan_tier (default 'free'). Add 3 indexes. Test both forward and backward migration before merging — rollback must work.
Depends on: BILL-01  |  Blocks: everything
Claude Code prompt:
Read hydra-stripe-billing-spec.md → Database Schema.
Write the Alembic migration with upgrade() + downgrade().
Add 3 indexes. Give me run + rollback commands.
Day 2 — Tue May 6

Free tier + webhook infrastructure.

Goal: Free tier hard stops at limit. Webhook endpoint receives test events from Stripe CLI and logs them without error.
BILL-03Free tier enforcement middleware
Urgent0.5dbillingfree-tier
Intercept every workflow execution. Check limits (fixes: 5, doc runs: 1, cycles: 5) per tenant per rolling 30-day window. Raise FreeTierLimitReached on hard stop — triggers upgrade prompt. Increment counter only after successful execution. Paid tenants (plan_tier != 'free') skip the check entirely. Initialize free_tier_reset_at on first run.
Depends on: BILL-02
BILL-04Free tier reset job (rolling 30-day)
High2hbillingfree-tier
Cron runs hourly. Finds free tenants where free_tier_reset_at < NOW(). Zeros all three counters. Advances free_tier_reset_at by 30 days from previous value — not from NOW(). This preserves the rolling window. Must be idempotent: safe to run multiple times per hour without double-zeroing.
Depends on: BILL-02
BILL-07Webhook endpoint + Stripe signature verification
Urgent2hbillingwebhooks
Single endpoint: POST /api/webhooks/stripe. Verify Stripe-Signature header on every request — never skip. Return 400 only on invalid signature. Return 200 for all handled events, even if the handler throws an exception (log, don't crash). Log every event with event.id for replay tracing. Route by event.type to handler map.
Depends on: BILL-01
Day 3 — Wed May 7

Checkout + the one handler that matters most.

Goal: A free user can upgrade to paid. Stripe Checkout completes. invoice.paid fires. Access is provisioned. End-to-end in test mode.
BILL-05Checkout session endpoint
Urgent0.5dbillingcheckout
POST /api/billing/checkout — creates Stripe Customer on first call (stores cus_ ID immediately). Creates Checkout Session with mode=subscription, adjustable_quantity (min 1, max 500), returns session URL. Frontend redirects — never construct Checkout URL on the frontend. Disable submit button on click to prevent double-submit creating two Customers.
Depends on: BILL-01, BILL-02
BILL-06Post-checkout success page
High2hbillingcheckout
Retrieve Checkout Session by ID. Confirm payment_status=paid. Store subscription_id. Return "status": "provisioning" — do not provision here. Access is granted by invoice.paid webhook, not by this endpoint. Show "provisioning" state to user until webhook fires and DB updates.
Depends on: BILL-05
BILL-08invoice.paid — provision paid access
Urgent2hbillingwebhooks
The most important handler. Fires on every successful invoice payment — initial, renewal, and after 3DS authentication. Set plan_tier, stripe_subscription_status=active, stripe_current_period_end, stripe_seat_count in DB. Idempotency check required: if tenant is already active at this plan, return early without writing. This fires multiple times — duplicate writes would corrupt the period_end on each renewal.
Depends on: BILL-07
Day 4 — Thu May 8

Payment failure, 3DS, and the complex subscription.updated handler.

Goal: All lifecycle events covered. Failed payments set grace period. Status transitions handled correctly. subscription.updated covers all 4 change types.
BILL-09invoice.payment_failed + grace period
High2hbillingwebhooks
Set stripe_subscription_status = 'past_due'. Do not revoke access. Start 7-day grace period. Send notification to billing contact. Note: Stripe Smart Retries run before this event fires — payment_failed fires after Stripe has exhausted automatic retries, not on first decline. Also implement handler for invoice.payment_action_required: send customer an email with Stripe-hosted 3DS authentication link.
Depends on: BILL-07
BILL-12checkout.session.completed + subscription.created
High2hbillingwebhooks
checkout.session.completed: store subscription ID, do not provision yet — wait for invoice.paid. subscription.created: if status=incomplete, hold at free; if status=active, provision. Out-of-order risk: invoice.paid can arrive before subscription.created. Handle missing tenant lookup gracefully — log and return 200, do not crash.
Depends on: BILL-07
BILL-10customer.subscription.updated
Urgent0.5dbillingwebhooks
Inspect data.previous_attributes for what changed. Handle 4 distinct cases: (1) quantity changed → update stripe_seat_count; (2) status: past_due → active → re-provision access; (3) status → canceled/unpaid → schedule revocation at current_period_end, not now; (4) items[0][price] changed → update stripe_price_id and plan_tier via price_id_to_tier() helper. All cases must be idempotent.
Depends on: BILL-07
Day 5 — Fri May 9 · End of Sprint 1

Subscription management. Full backend complete.

Sprint 1 done when: All 8 webhooks fire without error. Firing same event twice does not corrupt state. Upgrade flow works. Seat + plan changes work.
BILL-11customer.subscription.deleted
High2hbillingwebhooks
Set stripe_subscription_status = 'canceled'. Do not revoke access immediately. Paid access continues until current_period_end — this is Stripe's contract with the customer. Schedule a job to flip plan_tier = 'free' at current_period_end. Access gating middleware reads stripe_current_period_end to allow access until that timestamp.
Depends on: BILL-07
BILL-13Seat update — proration preview + apply
High0.5dbillingsubscription-management
POST /api/billing/seats. Step 1 (preview): call POST /v1/invoices/create_preview, return proration amount to frontend before user confirms. Step 2 (apply): user confirms, call Subscription.modify with new quantity. Critical: capture proration_date = int(time.time()) once in step 1. Pass same timestamp to step 2. Never recalculate — Stripe calculates to the second, any gap changes the amount and causes customer disputes.
Depends on: BILL-08
BILL-14Plan upgrade endpoint (Team → Business)
High2hbillingsubscription-management
POST /api/billing/upgrade. Swap items[0][price] to Business price ID. Always explicitly pass current quantity in the modify call — Stripe resets quantity to 1 if not included when changing price. Same proration preview + apply pattern as BILL-13. Same proration_date timestamp rule applies.
Depends on: BILL-08
Sprint 2 · May 12–16 · 10 tickets

Access.
Frontend.
Testing.

Portal, access gating, all billing UI, and two full test days. The test days cannot be rushed — they are what tells you billing is safe to go live.
Day 6 — Mon May 12

Access gating, portal, billing API. Backend surface complete.

Goal: A free user hitting a paid endpoint gets a structured SubscriptionRequired error. A past_due tenant within grace period is allowed through. Every backend endpoint exists.
BILL-15Customer Portal session endpoint
High1hbillingsubscription-management
POST /api/billing/portal. Call stripe.billing_portal.Session.create with customer ID, portal config ID, and return URL. Return portal URL to frontend for redirect. Portal URL expires when the session ends — never cache. Generate a new session on every click of "Manage subscription."
Depends on: BILL-01
BILL-16Access gating middleware
Urgent0.5dbillingaccess-control
On each request to a paid feature, check plan_tier + stripe_subscription_status + stripe_current_period_end. Rules: active → allow. past_due, within 7d grace → allow. past_due, past 7d → block. canceled, before period_end → allow. canceled, after period_end → block. Return structured SubscriptionRequired error with upgrade URL.
Depends on: BILL-08, BILL-10
BILL-17Billing settings API
High0.5dbillingapi
GET /api/billing/status. Return: current plan_tier, stripe_seat_count, stripe_current_period_end, stripe_subscription_status, and portal URL. For free users: return current usage vs limits for all 3 counter types. Used by billing settings page and post-checkout success polling.
Depends on: BILL-08
Day 7 — Tue May 13

Free tier tests. Upgrade prompt UI.

Goal: Free tier test coverage complete across all 7 scenarios. Upgrade prompt component renders at every limit with correct copy.
BILL-22Free tier enforcement unit tests
High2hbillingtesting
7 required test scenarios — all must pass before Sprint 2 frontend work begins:
  1. Counter below limit → allow
  2. Counter at limit → block, raise FreeTierLimitReached
  3. Counter at limit, paid tenant → skip check, allow
  4. Counter at limit, reset job runs → zeroed, allow
  5. Reset job runs twice → not double-zeroed (idempotent)
  6. free_tier_reset_at is NULL on first run → initializes to now() + 30d
  7. Reset job advances reset_at by 30d from previous value, not from now()
Depends on: BILL-03, BILL-04
BILL-18Frontend — upgrade prompt components
High0.5dbillingfrontend
Reusable component triggered at each hard limit. Trigger points:
  • 5 fixes reached this 30-day window
  • 1 doc run reached this 30-day window
  • 5 Linear cycles reached this 30-day window
  • 5 repos connected (Team limit for repos)
  • Jira feature attempted (Business only)

Non-punitive copy from hydra-pricing-plg.md Section 4. CTA links to billing/upgrade page. Do not block the entire UI — show prompt alongside the locked feature.

Depends on: BILL-03
Day 8 — Wed May 14

Billing UI. Users can upgrade, view plan, and manage their subscription.

Goal: Full billing UI in place. A user can complete the entire billing flow without a support ticket.
BILL-19Frontend — billing/upgrade page
High0.75dbillingfrontend
Tier selector (Free / Team / Business). Seat count input with real-time price calculation. Monthly/annual toggle — changes price_id sent to backend. Upgrade button calls POST /api/billing/checkout, then redirects to the returned Stripe Checkout URL.

Required: Disable button after first click (prevent double Customer creation). Show proration preview for existing subscribers changing plan or seats. Display specific error message on backend failure — not a generic toast. Checkout URL from backend only — never construct it on the frontend.

Depends on: BILL-05
BILL-20Frontend — billing settings page
High0.5dbillingfrontend
Two states: Paid user: Current plan badge (Team / Business / Annual), seat count, next renewal date, "Manage subscription" button → calls POST /api/billing/portal → redirects to Customer Portal. Generate new portal URL on every click — never cache. Free user: Current usage vs limits (3 progress bars: fixes, doc runs, cycles). Days until 30-day reset. "Upgrade" CTA linking to billing/upgrade page.
Depends on: BILL-15, BILL-17
Day 9 — Thu May 15

Success page + full integration test suite.

Goal: Happy path and all 8 failure scenarios tested against Stripe test mode. Real webhook payloads, not mocked ones.
BILL-21Frontend — post-checkout success page
Medium2hbillingfrontend
"You're on Team / Business. Provisioning takes a moment." Poll GET /api/billing/status every 2s, up to 30s. Show "provisioning" spinner until plan_tier is updated in DB by the invoice.paid webhook. Provisioning is async — the success page cannot show access confirmed until the webhook has fired.
Depends on: BILL-06, BILL-17
BILL-23Stripe test mode — integration tests
High1dbillingtesting
Run all 8 scenarios against Stripe test mode. Verify DB state after each. Cannot be done with mocks — must use real Stripe test API and real webhook deliveries.
Depends on: BILL-05–BILL-12

BILL-23 — 8 Required Test Scenarios

ScenarioCard / MethodExpected state after
Successful upgrade (monthly)4242 4242 4242 4242plan_tier=team, status=active
Successful upgrade (annual)4242 4242 4242 4242plan_tier=team, annual price_id
Payment failure at checkout4000 0000 0000 0002User stays free, no sub created
Post-upgrade payment failure4000 0000 0000 0341status=past_due, access within grace
Grace period expirymanual advanceplan_tier=free, access revoked
Seat increase with prorationseat update APIstripe_seat_count updated, invoice line created
Team → Business upgradeupgrade APIplan_tier=business, quantity preserved
3DS / SCA (EU card)4000 0025 0000 3155payment_action_required handled, provisioned after auth
Day 10 — Fri May 16 · Ship Milestone

Webhook handler tests + pre-live security audit.

Ship when: All 24 tickets merged. Every event type fires cleanly. Same event fired twice does not corrupt state. Security audit passes.
BILL-24Webhook handler tests via Stripe CLI
High1dbillingtesting
Use stripe listen --forward-to localhost:8000/api/webhooks/stripe to replay test events for all 8 event types. Each event must be fired twice to verify idempotency — the second firing must be a no-op. Cover: failed payment → grace period → access revocation. Cancellation → access until period_end. subscription.updated with seat change. subscription.updated with plan change.
Depends on: BILL-07–BILL-12

Pre-live security checklist (must pass before live keys)

!
No Stripe keys in code or git history
grep -r "sk_test_" . → nothing. git log -p | grep sk_test → nothing.
Signature verification on every webhook request
No code path bypasses Webhook.construct_event().
All handlers idempotent
Verified by Day 10 suite — each event fired twice with no corruption.
No tenant with plan_tier paid + NULL subscription_id
Run data integrity query before switching to live keys.
DB migration rollback verified on Day 1
alembic downgrade -1 must succeed if needed.
Go-Live Procedure

10 steps. Run in order. Do not go live on a Friday.

1
All BILL-23 + BILL-24 scenarios pass clean
Do not skip this even if you're confident. Run them again.
2
Switch Stripe Dashboard to Live mode
Live and test mode are separate — different keys, different webhook secrets.
3
Create Products + Prices + Portal in Live mode
Same amounts. Save the live Price IDs.
4
Register webhook endpoint in Live mode
New webhook signing secret — different from test. Copy to env.
5
Update all 9 STRIPE_* env vars to live keys
All of them. Including STRIPE_WEBHOOK_SECRET.
6
Deploy
Verify deployment health before proceeding.
7
Smoke test with a real card
Real $20 charge. Verify provisioning in DB. Refund immediately.
8
Monitor Stripe webhook logs for 30 minutes
Dashboard → Developers → Webhooks → endpoint delivery log.
9
Monitor application logs for handler exceptions
Any exception in a handler must be investigated immediately.
!
If anything fails: execute rollback immediately
Set BILLING_ENABLED=false. All tenants treated as free. Fix, re-run checklist, try again.
Incident Runbooks

Five scenarios. All of them will happen.

Incident 1 — Customer paid, no access
Check Stripe Dashboard: did invoice.paid deliver? Check app logs for webhook receipt. Check DB stripe_subscription_status. If webhook received but DB not updated: replay event from Dashboard. Emergency: manually set plan_tier and stripe_subscription_status while investigating root cause.
Incident 2 — Webhook endpoint returning 5xx
Stripe retries up to 5 times over 72 hours. You have time. Check application logs for the exception. Fix and deploy. Stripe will retry automatically. If retry window has passed: replay from Dashboard using the "Resend" button on each event.
Incident 3 — Double billing
Check for duplicate subscriptions on the customer in Dashboard. Cancel the duplicate immediately. Issue full refund. Root cause: usually double-submit on the checkout endpoint — button was not disabled on click. Fix: disable button on first click in BILL-19.
Incident 4 — Proration amount mismatch
Customer disputes charge differing from preview amount. Check logs for proration_date in preview call vs apply call. If different: this is the drift bug — issue partial refund, fix BILL-13 to pass preview's proration_date to the apply call. If same: cent-level variance is expected, explain to customer.
Incident 5 — Customer can't cancel
Portal URL may have expired or the portal endpoint is broken. Tyler can cancel directly from Stripe Dashboard in under 30 seconds. Do not make a customer wait for a code fix to cancel their subscription. Fix the portal session endpoint after the customer is handled.
Rollback plan
Option A (preferred): Set BILLING_ENABLED=false env var. All tenants treated as free. Safe for PLG — users prefer free access over an error. No deploy required.

Option B: alembic downgrade -1 removes billing columns.

Option C: Disable webhook endpoint from Stripe Dashboard to stop processing events while debugging.
Post-Ship — Week of May 19

Run Hydra on the billing module.
First internal dog-fooding target.

1
hydra discover
Index the billing module. Webhook handlers, DB queries, access gating middleware, checkout endpoints.
2
hydra audit
Run the full audit gauntlet. Catches: webhook signature bypass paths, accidentally logged Stripe keys, race conditions in checkout, missing idempotency checks, unhandled Stripe API exceptions.
3
hydra fix
Merge whatever Audit surfaces. Security findings first, then logic issues.
4
hydra improve
Optimize webhook handlers and DB query patterns. Billing will have N+1 queries on tenant lookups that only Hydra will catch at scale.

Billing is the right first internal target. Bounded scope. Clear inputs and outputs. Real production risk if wrong. If Hydra can audit and improve its own payment code, that is a proof point worth showing.

All reference files

  • hydra-stripe-billing-research.md — Stripe API research, primary citations, 24 Kanban cards
  • hydra-stripe-billing-spec.md — Full Python implementation spec, every endpoint + handler
  • hydra-stripe-billing-roadmap.md — Extended roadmap with security checklist, runbooks, rollback
  • hydra-stripe-billing-linear.csv — Import: Linear → Settings → Import → CSV
  • hydra-billing-deck.html — This deck