Skip to content

Plan Model

Plan lives on the org, never on the user. A user's effective tier is always the tier of their active org.

See also: docs/SPEC.org-plan-model.md in relay-platform for the full contract.


Two planes

Plane Org type is_personal Tiers
A — Personal Single-member true beta · free · pro
B — Team Multi-member, seat-billed false team · enterprise

Plane guards (shipped in PR #1241): personal orgs cannot gain members or a team plan. These are enforced in org_billing.tier — do not write to org_quotas for enforcement.


Tier reference

Tier Who How assigned
beta EAP cohort — grandfathered early access Admin-set at signup via joined_via='eap'
free Default after EAP graduation (no card) or grace expiry System sweep job or admin
pro Paying personal org Self-serve upgrade (step 6) or admin-set
grace EAP graduated, no card, within grace window Admin-triggered graduation, no card on file
team Seat-billed org Admin or Stripe
enterprise Custom contract Admin only

Back-compat: standard is a permanent alias for pro in all code paths — existing rows with tier='standard' resolve as Pro. Never write standard for new records.


Early Access Program (EAP)

EAP users land at tier='beta' with joined_via='eap'. Beta is indefinite — no auto-expiry until graduation.

Graduation (Step 4 — in progress)

Admin triggers graduation via console. Platform logic:

card on file → tier = 'pro'
no card      → tier = 'grace', grace_expires_at = now() + 30d

Grace sweep job (in loop.py, daily): - Query orgs where grace_expires_at <= now() - Set tier = 'free', clear grace_expires_at - Notify user

Notifications on plan change

  • Beta → Pro: "Your account has been upgraded to Pro"
  • Beta → Grace: "Your trial ends {date} — add a payment method to continue on Pro"
  • Grace → Free: "Your trial has ended — you're now on the free plan"
  • Free → Pro: upgrade confirmation

Admin controls (console)

All plan changes go through POST /api/admin/v1/orgs/{id}/plan (or org_plan_update). Changes are two-way synced with Stripe — a console change updates Stripe, and a Stripe webhook mirrors back.

Available admin actions: - Set tier directly (any tier) - Trigger EAP graduation - Set/clear grace_expires_at - View plan history (Stripe events via processor_events table)


Self-serve upgrade (Step 6 — planned)

Personal org owners will be able to upgrade free → pro via: 1. Plan selection screen in app (or settings) 2. Stripe checkout session 3. On checkout.session.completed webhook: tier → pro immediately

Downgrade (pro → free) at end of billing period.


Loop program

EAP users get joined_via='eap' stamped at signup. Loop enrollment is gated behind LOOP_ENABLED flag (currently off). When enabled, this hook fires at EAP signup — program details TBD.


Finance tracking

Plan-related revenue flows through: - processor_events — raw Stripe webhook payloads - finance_transactions — normalized money movements - discount_commitments — Loop early access, referral, coupon discounts - revenue_snapshots — daily MRR/ARR (pulled live from Stripe subscriptions)

Known gaps: churn tracking fields (churned_revenue, new_subs, cancelled_subs) are always 0 in snapshots — not yet wired. checkout.session.completed not handled in StripeAdapter.


Last updated: 2026-06-14