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):
- Master switch off → everything allowed (testing mode).
- The tier is marked free in config (e.g. Basic = boards & tracking stays free forever) → allowed.
- Active subscription at that tier or higher, not expired → allowed. (Advanced unlocks Mid; Mid does NOT unlock Advanced.)
- Free trial: within
trialDaysof workspace creation → allowed, with “N days left” shown. This is the “let them use it for a month, then charge” mechanism — settrialDays: 30. - 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.