DOCUMENTATION HUB·LEVEL_2

Epic 17 — Billing & Entitlement Architecture (One-Page Map)

REF_PATH: technical/epic17-billing-architectureSOURCE: APP_DOCUMENTS_DB

Epic 17 — Billing & Entitlement Architecture (One-Page Map)

Release: v0.1.0-ga-epic17 · Branch baseline: 6361d276 (docs + marketing synced)
Audience: Engineering, platform admin, design-partner onboarding
Scope: Stripe-assisted tenant billing, dashboard gates, tier-scoped feature entitlements, Ironbloom module coupling


1. Design intent

Epic 17 moves Ironframe from demo-ready to sales-assisted commercial onboarding without self-serve subdomain sprawl. Money unlocks workspace access; plan tier (derived from tenant slug) unlocks module depth. All persisted money uses BigInt integer cents — never float.

PlaneResponsibility
Stripe ingressCryptographically verified webhooks → tenant_billing row
Entitlement coreTenantBilling.status + slug → plan tier → feature matrix
UX gateDashboard shell blocks PENDING / PAST_DUE; GLOBAL_ADMIN bypass
API gateServer routes call assertTenantFeatureEntitled (e.g. Ironquery export)

2. Data model

model TenantBilling {
  tenantSlug       String   @unique   // joins Tenant.slug
  stripeCustomerId String   @unique
  status           String   @default("PENDING")  // PENDING | ACTIVE | PAST_DUE
}
StatusGate behaviorTypical source
UNTRACKEDNo row → dashboard open (legacy / seed tenants)Pre-Epic-17 tenants
PENDINGDashboard blocked (except exempt routes)ensureTenantBillingPending on manual provision
ACTIVEDashboard open; tier matrix enforced on APIsStripe webhooks
PAST_DUEDashboard blockedFuture subscription lifecycle (not wired to Stripe subs yet)

Manual provision placeholder customer id: manual_pending_{slug} (app/lib/billing/constants.ts).


3. Dual Stripe ingress (canonical split)

flowchart LR
  subgraph stripe [Stripe]
    CS[checkout.session.completed]
    PI[payment_intent.succeeded]
  end
  subgraph ironframe [Ironframe :3000]
    W1["POST /api/webhooks/stripe"]
    W2["POST /api/billing/webhook"]
    TB[(tenant_billing)]
    PROV[corporateTenantProvisionCore]
    AUDIT[AuditLog operator ids]
  end
  CS --> W1
  W1 --> PROV
  PROV --> TB
  PI --> W2
  W2 --> TB
  W1 --> AUDIT
  W2 --> AUDIT
WebhookSecret envEventFulfillment
/api/webhooks/stripeSTRIPE_INSTANT_CHECKOUT_WEBHOOK_SECRET (fallback STRIPE_WEBHOOK_SECRET)checkout.session.completedProvision tenant + invite user + set ACTIVE (stripeInstantProvisionCore.ts)
/api/billing/webhookSTRIPE_BILLING_WEBHOOK_SECRETpayment_intent.succeededActivate billing only on existing slug (stripePaymentIntentCore.ts)

Metadata contract: tenant_slug / slug, stripe_customer_id, email; checkout adds companyName, amountTotalCents (BigInt parse).

Security: Both routes are gateway-shield exempt but require stripe-signature verification (verifyStripeWebhookEvent) — not validateIngressContext. Listed in STRIPE_WEBHOOK_PATHS for deployment quarantine bypass.


4. Plan tier → feature matrix

Tier resolves from tenant slug (tenantFeatureEntitlement.ts) in Phase 1. Phase 2 binds Stripe Price metadatabasePriceCents + commercial SKU on TenantBilling.

Commercial SKU bind (board-sanctioned)

Commercial SKUbasePriceCentsEngineering tierStripe bind (Phase 2)
Fintech Seed Gate3500000 ($35k/yr)BASELINEplan_sku=FINTECH_SEED
Series A Growth Shield7500000 ($75k/yr)SUSTAINABILITYplan_sku=SERIES_A_GROWTH
Vault Shieldcustom quoteVAULTplan_sku=VAULT_SHIELD

Full sales narrative: Pricing & Packaging.

Feature entitlements (slug-derived today)

Slug examplesPlan tierEntitled features
medshield, defense, acmecorpBASELINEGRC_DASHBOARD, IRONQUERY_EXPORT (quota 25/mo)
vaultbankVAULT+ EVIDENCE_LOCKER_WORM, BOARDROOM_AUDIT_LOGS (quota 200)
gridcoreSUSTAINABILITY+ SUSTAINABILITY_ANALYTICS, CARBON_PULSE (quota 100)

Ironbloom coupling: Carbon pulse and sustainability analytics require SUSTAINABILITY tier and ACTIVE billing. Physical-unit ingress (kWh, liters, km) remains independent of Stripe; live Electricity Maps pulls are optional metered API cost with DB/static fallback.

API enforcement today: POST /api/ironquery/exportassertTenantFeatureEntitled(..., "IRONQUERY_EXPORT"). Expand matrix before GA.


5. Operator & user surfaces

SurfacePathRole
Dashboard billing gateapp/(dashboard)/layout.tsxDashboardBillingGateBlocks children when PENDING/PAST_DUE; GLOBAL_ADMIN bypass
In-app hold noticeBillingSuspensionNoticeShown inside dashboard shell when blocked
Public hold page/account/billing-holdStandalone degradation + checkout link (NEXT_PUBLIC_STRIPE_COMMAND_TIER_CHECKOUT_URL)
Admin provision/admin/onboardingbilling_gate: false — operators can provision while tenant is PENDING
Route manifestconfig/route-manifest.v0.1.0-ga-epic17.jsonDocuments billing_gate: true on integrity, cockpit, evidence, trust, etc.

6. Onboarding sequence (design partner)

  1. GLOBAL_ADMIN mints workspace invitation → corporate provision (corporateTenantProvisionCore) → ensureTenantBillingPending.
  2. Operator sends Stripe Payment Link / Checkout with metadata (tenant_slug, customer email).
  3. Path A — New tenant: checkout.session.completed → full provision + ACTIVE.
  4. Path B — Existing tenant: payment_intent.succeeded → flip PENDINGACTIVE only.
  5. User signs in → dashboard gate clears → tier-gated APIs apply.

7. Cost profile (summary)

EnvironmentBilling architecture cost
CI / Vitest$0 LLM (mocked @google/genai); local Postgres
Manual dev with live keysGemini, Resend, Stripe test mode, Electricity Maps — provider-metered
ProductionStripe transaction fees; Resend beyond free tier; optional Electricity Maps

Native BigInt ledgers eliminate float drift, not hosting or processor fees.


8. Open items (pre-GA)

  • Wire customer.subscription.updated/deletedPAST_DUE / cancel (Phase 2 lifecycle).
  • Commit / wire tenantFeatureEntitlement.ts + /account/billing-hold if still unstaged on branch.
  • Expand assertTenantFeatureEntitled to all billing_gate: true routes in manifest.
  • Phase 2: add basePriceCents, planSku, optional stripeSubscriptionId to TenantBilling.
  • Phase 2: Stripe Price objects for Fintech Seed / Series A Growth; checkout metadata contract.
  • Document env block in .env.example: dual webhook secrets, checkout URL, credential mode (STRIPE_CREDENTIAL_MODE).

10. Phase 2 roadmap — commercial bind (next sprint)

Goal: Connect board flat-fee SKUs to Stripe without replacing Phase 1 TenantBilling.

flowchart LR
  SKU[Stripe Price + metadata] --> WH[checkout.session.completed]
  WH --> TB[TenantBilling extended]
  TB --> BASE[basePriceCents BigInt]
  TB --> PLAN[planSku enum]
  SUB[subscription.updated] --> GRACE[PAST_DUE grace window]
  GRACE --> HOLD[Dashboard block after day 14]
Work itemDetail
Schema extensionAdd basePriceCents BigInt?, planSku String?, stripeSubscriptionId String? to tenant_billing
Webhook handlercustomer.subscription.updatedPAST_DUE; deletedPENDING or archived
Grace horizonWarn in-app during PAST_DUE; hard DashboardBillingGate block after configurable offset (default 14 days)
Checkout metadataplan_sku, tenant_slug, base_price_cents validated on ingest
DocsKeep pricing-and-packaging.md as commercial source of truth

Explicit non-goals for Phase 2: Per-user seat metering, new TenantSubscription table rename, overage invoicing.


11. Phase 3 roadmap — dual-engine overage (RFC)

Principle (board + TAS): Money and physics never share a persistence row. Phase 3 adds reconciliation, not a parallel telemetry schema.

EngineStorage (existing — do not duplicate)Contents
Billingtenant_billing + future invoice ledgerbasePriceCents, status, Stripe ids
IronbloomSustainabilityMetric, CarbonPulseSample, threat physical telemetry JSON, gridcore_carbon_coefficientskWh, liters, km, gCO₂eq — no price fields
[Stripe subscription events] ──► TenantBilling (cents, status)
[Threat / Kimbot / Carbon pulse] ──► Existing physical tables (units only)
                                              │
                                              ▼
                              Monthly reconciliation job (BigInt)
                                              │
                                              ▼
                                    Overage invoice (Stripe Invoice API)

Ingress rule: Reject monetary keys (price, cost, currency) at Irongate/Kimbot/Ironbloom ingress boundaries — extend existing validators; do not add IronbloomMetricLog or app/api/ironbloom/ingest unless TAS-amended.

Overage formula:

// Phase 3 — app/lib/billing/overageReconciliation.ts (planned)
export function computeOverageInvoiceCents(
  basePriceCents: bigint,
  overageUnits: bigint,
  ratePerUnitCents: bigint,
): bigint {
  return basePriceCents + overageUnits * ratePerUnitCents;
}

12. Competitive & cost context

QuestionAnswer
vs Vanta/Drata/SprintoPremium flat annual vs per-seat escalation; deeper ALE + agents + Irongate
vs ServiceNow/OptroTransparent mid-market fee vs opaque enterprise SI
Local dev cost$0 LLM in CI (mocked Gemini); $0 Postgres math
Production metersStripe transaction fees, Resend, optional Electricity Maps

See pricing-and-packaging.md § Competitive positioning.


9. Key file index

ConcernPath
Stripe configconfig/stripe.ts
Status constantsapp/lib/billing/constants.ts
Entitlement resolverapp/lib/billing/tenantBillingEntitlement.ts
Feature matrixapp/lib/auth/tenantFeatureEntitlement.ts
Checkout fulfillmentapp/lib/server/stripeInstantProvisionCore.ts
Payment-intent fulfillmentapp/lib/server/stripePaymentIntentCore.ts
Teststests/unit/stripeConfig.test.ts, tests/unit/tenantFeatureEntitlement.test.ts

Related: Architecture & API · Pricing & Packaging · Monetization blueprint · Feature glossary BILLING-002