Billing & Subscription
Billing & Subscription
Stripe-powered subscription management, plan limits, and paywall enforcement.
Overview
The Billing system manages subscription tiers (Free and Pro) via Stripe integration. It enforces four counted free-tier limits — projects, calculator runs, time-tracking projects and templates — and gates cloud sync and collaboration behind Pro. There is no storage quota and no collaborator-slot limit: nothing in app/src defines either. Checkout sessions and the customer portal are handled by Edge Functions, and usage is tracked for paywall enforcement.
Requirements
Functional Requirements
| ID | Requirement | Acceptance Criteria | Priority | Source |
|---|---|---|---|---|
| REQ-BIL-001 | The system shall enforce Free tier limits: 2 projects, 5 calculator runs, 2 time-tracking projects, 1 template1, no cloud sync, no collaboration | Create 3rd project → paywall modal; 6th calculator run → paywall; sync toggle → upgrade prompt; share button → upgrade prompt | Must | paywallLimits.js |
| REQ-BIL-002 | The system shall support upgrading to Pro tier via Stripe Checkout | Click Upgrade → redirect to Stripe Checkout; complete payment → return to app with Pro tier active | Must | stripe-create-checkout-session |
| REQ-BIL-003 | The system shall route "Manage Subscription" to the rail that sold the subscription — the Stripe Customer Portal, or Apple's subscription screen for an IAP purchase | Stripe-sold → redirect to Stripe Portal; Apple-sold → itms-apps:// deep link (REQ-IAP-012); cancel/resume/change plan → reflected in app | Must | stripe-create-portal-session |
| REQ-BIL-004 | The system shall respond to Stripe webhook events for subscription lifecycle changes | Subscription created/updated/deleted → app reflects current status; no manual refresh needed | Must | stripe-webhook |
| REQ-BIL-005 | The system shall display a paywall modal with feature comparison when limits are hit | Hit limit → modal shows Free vs Pro features; Upgrade button → Stripe Checkout; Dismiss → stays on Free | Must | PaywallModal |
| REQ-BIL-006 | The system shall track anonymous usage to attribute history to account on sign-up | Use app anonymously → usage tracked; create account → usage history attributed to new account | Should | anonymous_usage_ledger |
| REQ-BIL-007 | The system shall display current plan status and usage in Settings | Open Settings → plan name shown; usage bar (projects used/limit); upgrade/manage buttons visible | Must | settings |
| REQ-BIL-008 | The system shall support coupon codes and promotional pricing | Enter coupon at checkout → discount applied; invalid code → error message; expired coupon → error | Should | stripe-create-checkout-session |
Non-Functional Requirements
| ID | Requirement | Metric | Priority |
|---|---|---|---|
| NFR-BIL-001 | Subscription status shall be cached locally for 24 h so the paywall does not re-query on every check | Performance | Should |
Two latency NFRs (a 50 ms paywall check and 5 s webhook processing) were removed in v0.773.8: nothing measured either, and an unmeasured budget in a requirements table reads as a guarantee. The cache TTL above is real —
SUBSCRIPTION_CACHE_TTL_MSinusePaywallModel.js.
Data Requirements
- Subscription status: the
user_entitlementsSupabase table, read throughsubscriptionService.getSubscriptionStatus(userId)and cached client-side. It is not kept in Supabase user metadata — a client that could write its own metadata could grant itself Pro. - Usage tracking:
anonymous_usage_ledgertable for pre-signup tracking, checked by theanonymous-usage-checkedge function - Edge Functions:
stripe-create-checkout-session,stripe-create-portal-session,stripe-webhook,anonymous-usage-check. There is noget-subscription-statusfunction; the client queriesuser_entitlementsdirectly under RLS. - Client-side:
@stripe/stripe-jsfor Checkout redirect
Constraints & Limits
| Tier | Projects1 | Calculator runs | Time-tracking projects | Templates | Cloud Sync | Collaboration |
|---|---|---|---|---|---|---|
| Free | 2 | 5 | 2 | 1 | ❌ | ❌ |
| Pro | Unlimited | Unlimited | Unlimited | Unlimited | ✅ | ✅ |
PDF export is not tier-limited. This table carried a "Watermark / Full" column until v0.773.8; there
is no free-tier watermark anywhere in the app. Watermarks exist only on invoices, driven by
document state (DRAFT, PENDING, WITHDRAWN — see app/src/data/pdf/invoice/applyWatermark.js),
and nothing under app/src/data/pdf/ reads the subscription tier at all.
Offline Behavior
- Subscription status cached locally — paywall enforcement works offline
- Stripe Checkout and Portal require network (redirect to stripe.com)
- Webhook events processed server-side (no offline impact)
Dependencies
- Requires: Authentication (REQ-AUTH-*) — billing linked to user account
- Required by: Projects — project creation limits
- Required by: Collaboration — collaboration gate
- Required by: Sync Engine — sync gate
Plan Comparison
Free Tier
- Up to 2 projects, 5 calculator runs, 2 time-tracking projects, 1 template1
- Full offline functionality
- PDF export — complete, and not watermarked
- The full accounting suite (see below)
- No account required
Pro Tier
- Unlimited projects, calculator runs, time-tracking projects and templates
- Cloud sync across devices
- Real-time collaboration
- Priority support
The accounting suite is NOT a Pro feature. This list claimed it was "Pro-only since v0.441.0 (#256)" until v0.773.8.
isAccountingEnabled()inapp/src/data/auth/featureAccess.jsreturnstrueunconditionally — the suite graduated from its owner-only gate to general availability, and invoices, quotes, expenses, banking and tax filings are available on the Free tier. Nobody should buy Pro to unlock them.
Managing Your Subscription
Upgrading
- Hit a limit (or go to Settings → Account → Subscription)
- Click Upgrade to Pro
- Complete payment via Stripe Checkout
- Pro features activate immediately
Managing / Canceling
- Go to Settings → Account → Subscription
- Click Manage Subscription
- Where it takes you depends on which rail sold the subscription:
- bought through Stripe → the Stripe Customer Portal opens; update payment, cancel, or resume
- bought through Apple in-app purchase → an
itms-apps://deep link into Apple's Subscriptions screen, because Apple does not let a third party cancel an IAP (REQ-IAP-012)
Tips
- Start free: The Free tier is fully functional for small projects
- Upgrade when you need sync: Cloud backup is the biggest Pro advantage
- Cancel anytime: No long-term commitment, cancel from the Portal
- Usage shown in Settings: Monitor your project count before hitting limits
Related Documentation
Last Updated: 2026-09-09 Version: 0.790.2
Footnotes
-
Source of truth for all four free-tier limits is
app/src/data/user/paywallLimits.js:FREE_PROJECT_LIMIT = 2,FREE_CALCULATOR_LIMIT = 5,FREE_TIME_TRACKING_LIMIT = 2,FREE_TEMPLATE_LIMIT = 1.usePaywallModel.jsimports them (line 26) and exposes them aspaywall.free*Limit; it does not define them. Each limit is mirrored server-side — that module's own header lists the migrations and theanonymous-usage-checkfunction that enforce them. ↩ ↩2 ↩3
