decision
ADR-0012: Stripe Integration — Customer-OAuth-Only in V1
ADR-0012 (Proposed (resolves SPEC Open Q4; Ally Roger sign-off pending per Phase 1 friction-points doc), 2026-05-14): Stripe Integration — Customer-OAuth-Only in V1.
Status: Proposed (resolves SPEC Open Q4; Ally Roger sign-off pending per Phase 1 friction-points doc)
Date: 2026-05-14
Deciders: Seth (Lead Architect), Blair (CEO), Ally Roger (legal / billing sign-off)
Context
The SPEC (§5.5 cost, §8.1 data sources, §7.4 billing) describes two Stripe integration paths for MemberIntel V1:
- Stripe Connect — for MP customers already on the Caseproof platform, where Connect onboarding gives MemberIntel access to the customer’s Stripe data (revenue, churn, dunning metrics).
- Stripe OAuth into the customer’s own Stripe account — for non-Connect customers who authorize MemberIntel to read their Stripe data.
The SPEC flags this as Open Q4: “Stripe Connect vs. customer-OAuth split — the scope of OAuth flow depends on this.” The Phase 1 friction-points document adds that the Stripe integration approach needs Ally Roger sign-off.
V1 also requires a billing flow for the Pro tier ($29/mo per site, SPEC §5.4). Caseproof already has a Stripe account for billing. The question is how to integrate Stripe for both billing (subscriptions) and data access (reading the customer’s own Stripe metrics).
The SPEC’s cost discipline mandate (§5.5) requires server-side enforcement of tier-based quotas. ADR-0001 already established check_and_consume in entitlement/service.py as the single enforcement point. The billing integration must feed tier changes into that entitlement service.
Decision
V1 uses Stripe Checkout for Pro subscriptions — not Stripe Connect. The integration is customer-OAuth-only for two distinct concerns:
-
Billing (subscription management): MemberIntel creates a Stripe Customer and a Checkout Session when a Free user upgrades to Pro. Stripe handles card collection, recurring billing, failed-payment dunning (SPEC §7.4 US-B3: 3 failures then 7-day grace then downgrade). Webhooks propagate subscription state changes back to MemberIntel.
-
Stripe data access (revenue/churn metrics): V1 uses customer-OAuth into the customer’s own Stripe account — the same OAuth flow the SPEC describes for non-Connect customers. Customers already on the Caseproof platform (with existing Connect authorizations for other Caseproof products) will also go through this MemberIntel-specific OAuth, not a Connect-driven data pull. This avoids the V1 dependency on Connect onboarding, platform liability, and marketplace commission structure.
Webhook-driven entitlement updates. Stripe webhooks (checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed) write to a stripe_events_processed table (idempotency key: Stripe event ID). The handler for each event updates the users.tier column and triggers the entitlement service to refresh quotas. This is the same check_and_consume gate established in ADR-0001 — no separate billing-aware code paths.
Schema shape:
stripe_customers— mapsuser_idtostripe_customer_id. Created at first Checkout Session.stripe_subscriptions— mapsstripe_customer_idtostripe_subscription_id,status,current_period_start/end,tier. Written by webhook handler.stripe_events_processed— idempotency log. Columns:stripe_event_id(PK),event_type,processed_at. Prevents double-processing on webhook retry.
Customer-OAuth flow for Stripe data access is deferred to a post-V1 data-ingestion slice. The SPEC’s Phase 1 scope includes “Stripe Connect query + Stripe OAuth for non-Connect customers” (§13), but the billing integration is the critical-path dependency for the Pro tier. The data-access OAuth is separable and ships when the MP-sync pipeline is ready — Stripe metrics without MP membership data are of limited value.
V1.5 will evaluate Stripe Connect for per-customer billing if the product needs marketplace-style onboarding, commission splits, or automatic provisioning for Caseproof platform customers. Connect is not rejected — it’s deferred.
Consequences
Positive:
- Fastest path to a working billing flow. Stripe Checkout is a hosted page — no PCI scope, no card-input UI to build, no custom dunning logic.
- Single Stripe account (Caseproof’s existing relationship). No Connect onboarding, no platform agreement, no marketplace commission.
- Webhook + idempotency key pattern is well-tested; Stripe guarantees at-least-once delivery and the
stripe_events_processedtable makes processing exactly-once. - Tier changes propagate to
entitlement/service.pythrough the samecheck_and_consumepath — no billing-specific bypass. - Deferring Connect keeps V1’s legal surface small (no platform agreement, no commission structure, no onboarding liability).
Negative / costs:
- Connect customers don’t get automatic data access in V1 — they must separately authorize MemberIntel’s Stripe OAuth for data read. This is a small UX friction for the subset of customers who are both on the Caseproof platform and MemberIntel Pro users.
- Stripe Checkout’s hosted page means limited UI customization for the upgrade flow. The two-click upgrade (SPEC US-B1) is “click upgrade → confirm on Stripe → redirect back” — two clicks on our side, but the Stripe page is between them.
- Webhook delivery is asynchronous. There is a window (typically seconds, up to minutes on retry) between a successful checkout and the entitlement update. The user may see Free-tier behavior for that window. Mitigation: the Checkout redirect can include a
?session_idparameter that the frontend can poll until the webhook processes. - Deferring Connect means V1 cannot offer automatic per-customer provisioning for Caseproof platform users. Each customer relationship is direct between MemberIntel and the subscriber.
Mitigations:
- The Checkout redirect includes
session_id; the frontend polls/api/v1/billing/checkout-status?session_id=...which returns{ "status": "active" }once the webhook has processed. This closes the entitlement-propagation gap without polling Stripe directly. - For Connect customers, the V1.5 evaluation will assess whether the UX friction of separate OAuth is material enough to warrant Connect’s platform agreement overhead.
- The
stripe_events_processedtable doubles as an audit log — useful for support and for the monthly cost-per-cohort dashboard (SPEC §5.5). - Dunning logic (SPEC §7.4 US-B3) is handled entirely by Stripe’s Smart Retries + our webhook handler for
invoice.payment_failed. No custom dunning code in V1.
Alternatives considered
- Stripe Connect (marketplace model) — rejected for V1. Connect requires a platform agreement, onboarding flow, commission structure, and marketplace liability. It solves the data-access question for Caseproof platform customers but introduces a billing-complexity and legal-surface cost that V1 doesn’t need. Deferred to V1.5 evaluation.
- Direct card processing (Stripe Elements / Payment Intents) — rejected: brings PCI scope, custom card-input UI, dunning logic, and failed-payment retry code. Stripe Checkout handles all of this. SPEC §7.4 US-B1 demands “two clicks, card on file, immediate billing” — Checkout delivers this with zero custom billing code.
- Third-party billing (Paddle, Lemon Squeezy) — rejected: Caseproof has an existing Stripe relationship; adding a second payment processor increases operational surface with no benefit in V1.
- Billing-agnostic entitlement (tier set by admin, no payment link) — rejected: the SPEC mandates self-serve upgrade/downgrade (US-B1, US-B2). A manual tier-provisioning system doesn’t meet the product requirement.
- Polling Stripe for subscription state instead of webhooks — rejected: polling introduces latency and API rate-limit consumption. Webhooks are near-real-time and the idempotency-table pattern is proven.