🔍

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 .”

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.