# The Monetization Switch — subscriptions you can turn on and off

*Built July 20, 2026, and proven live the same hour: billing was switched ON from the background (paywall appeared on the production app: "Mid plan — $29/month … Your free trial has ended"), then switched OFF (everything free again) — no app update, no redeploy. This doc is the logic, the controls, and the manual.*

## 1. The design in one paragraph

One Firestore document — **`config/billing`** — is the single source of truth for whether Trackmint is a free product or a subscription product. Every app reads it at startup; the owner (or the admin CLI) writes it. The master switch `enabled` defaults to **false**: while it's off — or the doc doesn't exist, or can't be read, or the user is in demo mode — the entire app is free, exactly right for the TestFlight stage. Flipping it on activates per-tier pricing, the free-trial clock, and the paywall, instantly, for every user, without shipping a build.

## 2. The logic (what gets a user past the paywall)

Checked in order by one pure, unit-tested function (`tierEntitlement`, 14 tests):

1. **Master switch off** → everything allowed (testing mode).
2. **The tier is marked free** in config (e.g. Basic = boards & tracking stays free forever) → allowed.
3. **Active subscription** at that tier or higher, not expired → allowed. (Advanced unlocks Mid; Mid does NOT unlock Advanced.)
4. **Free trial**: within `trialDays` of workspace creation → allowed, with "N days left" shown. This is the "let them use it for a month, then charge" mechanism — set `trialDays: 30`.
5. Otherwise → **paywall**: tier name, monthly price (with any per-vertical override), your editable message, trial status, and a Subscribe button (checkout arrives with Stripe; until then you grant subscriptions manually).

What's gated: **Billing + Invoices** require the Mid tier; **AI document scan** requires Advanced. Boards, timers, and task management are the Basic tier — recommended to keep free as the funnel (doc 29).

## 3. The controls (admin tool — how YOU run it)

```
billing:get                          # show current config
billing:enable / billing:disable     # THE master switch
billing:set-tier Basic free          # make a level free…
billing:set-tier Mid 29              # …or priced
billing:set-tier Advanced 59
billing:set-trial 30                 # "free for a month, then subscribe"
billing:set-pack legal 79            # vertical pricing: law firms pay $79 for Advanced
billing:set-message "Subscribe to unlock billing & invoicing."

sub:status <uid>                     # a user's trial start + subscription
sub:grant <uid> Mid 365              # manually subscribe someone (365 days; omit = forever)
sub:revoke <uid>                     # cancel
```

Until the service-account key exists, the same switches work from the owner's signed-in browser session (Firestore rules published today: any signed-in app can *read* `config/billing`; only the owner account can *write* it — that's how today's live demo was performed).

## 4. What the user sees

**Testing mode (now):** nothing changed — Setup → Account shows "Plan: testing mode — all features free."
**Billing on, in trial:** app fully works; Account shows "free trial — N days left."
**Billing on, trial over, no subscription:** Billing/Invoices/Docs screens are replaced by the SUBSCRIPTION card (price, message, Subscribe); the board, timer, and tasks keep working — a user never loses access to their work, only to the money features.
**Subscribed:** Account shows "Plan: Mid — subscribed until <date>."

## 5. Safety properties

Fail-open to FREE, never to locked: any error reading config (offline, rules, missing doc) means testing mode. Demo accounts are never gated. Data is never locked — a lapsed subscriber keeps every board and record, and Basic-tier features, forever. Config is normalized defensively (garbage in the doc → safe defaults). The workspace's `createdAt` stamp is set once at first setup and never overwritten, so trials can't be reset by reinstalling.

## 6. When Stripe arrives (the missing 2%)

Stripe Checkout + a webhook writes the exact same `subscription` object the admin tool writes today (`{plan, status, paidUntil}`) — nothing else changes. That webhook needs a server (Firebase Blaze plan → Barry adds the card). Until then, "Subscribe" is a manual concierge motion: user asks → `sub:grant` → they're live in seconds. At early-stage volume that's not a workaround, it's a sales call.
