DEV Community

shaojie gong
shaojie gong

Posted on

How do I start charging later without punishing the people who showed up first?

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 (3)

Collapse
 
mike_viewfy profile image
Mike Kovetsky

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.

Collapse
 
shaojie profile image
shaojie gong

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

Collapse
 
sarahpan profile image
Sarah Pan

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?