Gating content behind a payment: five constraints that decide the design
DEV Community

Gating content behind a payment: five constraints that decide the design

Six months of building a content locker, which is a page that hands over a file, a licence key or a Discord role once somebody pays or completes a sponsored offer, left me with a short list of things that decide the design.

Not one of them is the interface. The interface is the easy, enjoyable part, and it is also the part that teaches you nothing about whether the thing works.

1. "Who is allowed in" has to be answered by the server, every time

The instinct is to build a page that unlocks: click, a state changes, the button appears. It takes a curious visitor about four seconds to find the thing the button was hiding and help themselves.

Sooner or later every gated product arrives at the same split. The browser can ask, and the server answers. The asset, the key, the role: none of it exists in the page until something that can't be edited has agreed. Everything else is presentation.

That sounds obvious written down. It is the single decision that determines how much of the rest of the system has to exist, because the moment the server is the one answering, you owe it an answer that is correct, cheap, and the same if it gets asked twice.

2. A payment arriving is an event, not a fact

The first version trusted the browser coming back from checkout. It is not the browser's job to tell you someone paid, and the browser is the least reliable witness in the chain: closed tabs, flaky mobile networks, people who pay and then hit back.

The lesson generalises past payments. Any external system that tells you "this happened" will tell you more than once: retries, duplicate deliveries, a queue that redelivers after a timeout. So the design question isn't "how do I handle the event". It is "what does my system look like when the same event arrives three times and only one of them is real".

The same applies in reverse. A refund or a chargeback is also an event, and one that most first versions ignore entirely, because nothing about the happy path suggests it.

3. Access that doesn't end isn't access, it's a subscription you lost

Granting a Discord role is one API call and a very satisfying demo. Taking it back is the part that decides whether the product is honest.

"Expires" needs a real definition:

  • what the member sees when it lapses
  • whether their history stays
  • whether renewing is instant
  • and what happens if they leave the server and come back

Those are product questions rather than plumbing, and they are the ones publishers ask about first.

What I didn't expect is how often the failure is not in the code but in the platform's own rules. A bot can only manage roles below its own, a deleted role leaves grants pointing at nothing, and members who rejoin come back empty. A nightly check of what should exist versus what does catches more of this than any amount of defensive coding.

4. Prices have a floor, and it isn't the one you think

Stripe's minimum charge for USD is 50 cents. Which means an aggressive discount doesn't produce a cheap sale. It produces an error at checkout that the customer experiences as "this site is broken".

const MIN_CHARGE_CENTS = 50; // Stripe's floor for USD
export function chargeFor(baseCents: number, discount = 0) {
  return Math.max(Math.round(baseCents * (1 - discount)), MIN_CHARGE_CENTS);
}

The code is trivial; the placement is not. If the page shows $0.40 and the API charges $0.50, you have a pricing bug that only ever affects your cheapest customers. Clamp where the number is displayed, and warn the person creating the discount when they save it, not the customer at checkout.

Underneath the technical floor there is an economic one. Below about a dollar the per-transaction fee is most of the sale, so "cheap" isn't a growth strategy. Working that out early is cheaper than learning it from a month of statements.

5. Where the page lives is part of the product

There are two ways to gate something: send the visitor through somebody else's interstitial, or serve the gate yourself. The first is a weekend of work. The second is where the brand lives.

A gate on a domain the publisher controls means their name, their terms, their colours, and a URL they are not embarrassed to put in a Discord message. It also means the gate is theirs in the eyes of a search engine, which forces a decision most people don't think about until later: a customer's portal shouldn't compete with your marketing site for the same query.

That decision, about what is indexable and on which hostname, is worth making in week one. Retrofitting it is a routing problem rather than a copy change.

None of these are exotic. They are the ordinary consequences of taking money from strangers and handing over something they can copy: decide on the server, assume every event repeats, define what "expired" means, respect the payment provider's limits, and treat the hostname as part of the product.

I built this as UnlockHub, which handles the unglamorous half: payment or offer in, key, file or expiring Discord role out, with publishers keeping 75% of each sale after the card fee. There is a walkthrough of selling access to a Discord server and the reference documentation lives at unlockhub.xyz/docs.

If you're building your own, the 50-cent clamp and a real definition of "expires" are the two cheapest decisions you will make. Skipping them costs a quarter of the development time later.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.