My plan has always been: launch almost everything free, get people using it, and start charging for individual features later, once there's a reason to. I wrote earlier about the remote switch that lets me flip a feature to paid without shipping a new release.
But there's a nasty problem hiding inside "charge later," and it took me a while to name it.
The day I flip, say, flashcard export from free to paid — what happens to the person who's been using flashcard export every day for three months? Under the naive version of my switch, they open the app tomorrow and a thing they relied on is suddenly locked. I just punished my most engaged user. Worse: I punished them specifically because they showed up early, before I figured out how to make money. The people who gave me my first reviews and told their friends would be the exact people I paywall first.
That's backwards. Early users should be rewarded for being early, not billed for it.
The answer is an old idea with an ugly name: grandfathering. When you start charging for something, the people who were already there keep it free — forever.
The tricky part is that my switch had no sense of time. It knew "is this feature paid?" but not "paid for whom, since when?" So I taught it.
Two pieces. First, every install now quietly records when it first ran — an installedAt timestamp, written the very first time the extension loads. Second, my paywall switch can now hold more than just on/off. Instead of "flashcard export = paid," I can set "flashcard export = paid for anyone who installed after March 1st." Everyone with an installedAt before that date sails through free, permanently. Everyone after pays. Same one-value flip, no new release — it just now carries a "since when" alongside the "paid."
In code it's a tiny change: the gate value went from a boolean to either a boolean or a little object with a paid flag and a since date. The check became "if you're not paid, and your install predates the cutoff, you're exempt." That's it. But the behavior it buys is exactly the promise I want to make: get here early, keep it free, no asterisks.
One honest limitation. installedAt is only accurate for people who install after I shipped this timestamp logic. Anyone already using the extension before that gets stamped with "now" the first time they upgrade — so they look slightly newer than they really are. The practical consequence is oddly motivating: the earlier I ship this, the more of my real early users get correctly grandfathered. Deploying the loyalty machinery is itself time-sensitive.
What I like about this is that it dissolves a conflict I thought I had to live with. "Grow with free, monetize later" felt like it was in tension with "don't betray the people who believed in you early." It isn't — you just need your paywall to remember who was here first. The strategy decides when to charge; the timestamp decides who's exempt. Two separate knobs, no contradiction.
I haven't charged anyone a cent yet. But the machine that will, someday, already knows to let my first users through for free. That feels like the right thing to build before I need it, not after someone gets burned.
How do you handle your earliest users when pricing changes — grandfather them forever, give them a window, or something else?
— building NotebookBloom in public, #15
Top comments (4)
the switch doesn't need history, the entitlement table does. a remote flag answers "is this feature paid", which is the wrong question. the right one is "does this user have a grant", and that's a row you write once: user_id, feature, granted_at, reason. then the flag flips and nobody's grant evaporates.
on the event, i'd use first actual use of that feature, not first login, and i'd backfill it from your export logs before you announce anything, because the day you announce is the day a bunch of curious people go export one deck to get in under the wire.
the thing that usually bites later is re-entry. someone grandfathered goes quiet for seven months, comes back on a new device, and your grant lookup is keyed to something that changed.
"the switch doesn't need history, the entitlement table does" — that's the line that reframed the whole thing for me. i was answering "is this feature paid" and calling it grandfathering, when the durable question was always "does this user have a grant." a cutoff date is me trying to derive a grant from a proxy (install time) instead of just recording one. the proxy's cheaper right up until it's wrong.
and the re-entry case is the one that actually scares me, because you put your finger on exactly where mine is fragile. my installedAt doesn't live in a table — it lives in chrome.storage.local. so it's device-local and profile-local. a grandfathered free user who reinstalls, switches laptops, or clears storage gets stamped now() again and silently drops out of the group i promised to protect. that's your "grant lookup keyed to something that changed," except mine changes on a browser reinstall.
here's the honest complication that pushed me toward the proxy in the first place, though. i store zero user data server-side on purpose — the app's data lives in the user's own Google Drive, and my KV only holds Stripe/license stuff. and free users never authenticate; i only have a verified identity (a Google email) for people who've paid. so the cruel irony is that the users i most want to grandfather — the early free ones — are exactly the population i have no server-side id to key a grant table on. user_id, feature, granted_at, reason assumes a user_id i don't have for the people who matter here.
where your comment actually moved me: the one server-side thing that already follows a free user across devices and survives a reinstall is their own Drive appdata, which i already sync. so the grant could live there — written once on first actual use, rehydrated on re-entry — and it stops being tied to the machine without me standing up an identity system or a user table i swore i wouldn't. not a clean central entitlement table, but a grant that travels with the person instead of the laptop.
and "first actual use, not first login" is just correct — i'm stamping too early. the backfill-before-you-announce point is sharp too; the only reason i can't do it your way is i don't keep export logs server-side to backfill from (same zero-data constraint biting me again). but you're dead right that announce day is get-in-under-the-wire day.
genuinely good comment. you found the hole i was already a little nervous about and named it better than i had.
Storing the grant in the user’s Drive appdata sounds like a much better fit for your zero-server-data constraint. I’m curious about one remaining edge case, though: if free users don’t authenticate, what lets the extension reliably reconnect to the same appdata after a reinstall? Is Google authorization already part of the sync flow, even though you don’t maintain your own user accounts?
you caught the exact assumption i waved past. yes — Google authorization is already the whole spine of the sync flow. the extension does an OAuth consent, gets a token, and from then on the user's Google account is the anchor: reinstall, new laptop, cleared storage, doesn't matter — they re-consent and the extension finds the same files again, because the files are tied to their Google identity, not to the machine. i don't run user accounts at all; i borrow Google's. so for anyone who's authed, re-entry is genuinely solved, and that's the part that makes the "grant travels with the person" idea work.
small correction on my own wording first, because it matters to your question: my sync doesn't actually use the hidden appdata folder anymore. it writes to a visible drive.file folder called NotebookBloom, on purpose — i switched off appdata because users couldn't see it and assumed nothing was syncing, and "you can watch your own data sit in your own Drive" is a selling point i didn't want to hide. functionally, for the grant idea, drive.file and appdata behave the same: scoped to my app, follows the Google account, survives a reinstall. so nothing about your question changes, i just don't want to claim appdata when the code says drive.file.
but here's where you actually pinned me, and it's sharper than the wording: all of that is true only for a user who has authed. and sync is a Pro feature. a free user, by default, never does the OAuth consent at all — that's the whole "free users have no identity i can key anything to" problem from earlier, just resurfacing one layer down. so the grant-in-Drive idea doesn't kill the re-entry hole for the exact people i most want to grandfather; it relocates it. it turns "your grant dies on reinstall" into "your grant survives reinstall if you authorized Google at least once." better, but not free.
which leaves me an honest fork i hadn't framed until you asked. either i offer free users an optional, one-tap Google authorization whose only job is to preserve their grandfathered status — opt-in, clearly not a login, just "let me remember you're an early user" — and accept that some won't take it and will stay best-effort/device-local. or i accept that a durable grant is a thing i can only truly promise to people who've already chosen to sync, and be upfront that for a purely local free user it's a courtesy, not a guarantee.
i think it's the first one, framed carefully, because a grandfather promise that quietly evaporates on reinstall is worse than one i'm honest about the edges of. either way — you found the seam in my own patch, one comment after i thought i'd sealed it. genuinely good catch.