Single-Use Access Code Paywall, Built Fail-Closed

Single-use access code paywall demo — fail-closed validation system built on Netlify

A single-use access code paywall is the simplest way to sell one-time access to digital content — no membership platform, no monthly fees, no user accounts. Here's exactly how I built one that holds up under real traffic, and the two mistakes that would have broken it silently.

If you're selling a single digital product — an ebook, a mini-course, a private report — you don't actually need a full membership platform with monthly fees and user accounts. You need one thing: a way to make sure each code you sell unlocks the content exactly once.

That sounds simple. It isn't, if you actually care about it holding up under real traffic. Here's what I learned building one from scratch, and why most "paywall in 10 lines of JavaScript" tutorials you'll find online are quietly broken.

Admin dashboard showing single-use access codes with used and available status tracking

The problem with the obvious approach

The naive version looks like this: store a list of valid codes somewhere, check the code the user typed against that list in the browser, and show the content if it matches.

This fails in two ways:

  1. It's checked client-side. Anyone can open dev tools, read the JavaScript, and see the full list of valid codes — or just skip the check entirely.
  2. It doesn't track usage. Even if you move the check to a server, a code that's valid once will stay valid forever unless something marks it as "used."

Neither of these is a hypothetical concern. If you're actually selling something, someone will eventually try to reuse a code, whether by accident (refreshing a page) or on purpose (sharing it with a friend).

What "fail-closed" actually means

The core design decision I made early: if anything goes wrong — the storage backend is unreachable, a request times out, whatever — the system should deny access by default, never grant it by default.

This sounds obvious written down, but it's the opposite of what a lot of code defaults to. A try/catch block that swallows an error and falls through to "show the content anyway" is an easy trap to fall into when you're optimizing for a smooth user experience. I'd rather show a genuine error message and ask someone to retry than silently leak access.

try {
  existing = await store.get(code, { consistency: "strong" });
} catch (err) {
  console.error("Storage read failed — rejecting to fail closed:", err);
  return json(500, { valid: false, reason: "storage_unavailable" });
}

Every failure path in the system follows this rule. No exceptions.

The race condition that almost got past me

Here's the part that surprised me. My first version checked "has this code been used?" and then, if not, wrote a record marking it used — two separate steps. That's fine most of the time, but it has a gap: if two requests for the same code arrive close enough together, both can pass the "has it been used?" check before either one finishes writing the "used" record. Both succeed. The single-use guarantee silently breaks.

The fix is to make the check itself the primary defense: read the record first with strong consistency, and if it already exists, reject immediately. I added a second, atomic layer on top — a conditional write that only succeeds if the key doesn't already exist — as backup protection for the narrow case of two truly simultaneous requests. But here's the part that surprised me during testing: that atomic "write-only-if-new" flag occasionally reported success on a key that already existed in the store. If I'd trusted it as my only defense, single-use enforcement would have silently broken in production. The read-before-write check is what actually catches that case.

This is the kind of bug that won't show up in casual testing. It only shows up under real concurrent load — which is exactly when you can least afford it, because that's when someone is actually trying to buy your product.

This is the kind of edge case that's easy to miss when you're building your own paywall under deadline pressure — it's exactly the sort of thing I check for when I build these systems for clients. More on that below.

Keeping the session secure after the code is used

Once a code checks out, the system needs to let that one visitor finish whatever they're doing (a quiz, a download flow) without re-checking the code on every step — but also without giving them a permanent, forgeable "I'm in" cookie.

I used a short-lived, signed token instead: a small payload with an expiry timestamp, signed with HMAC, verified in constant time (crypto.timingSafeEqual) rather than a simple string comparison. Constant-time comparison matters here because a naive === check leaks timing information that, in theory, lets an attacker guess a valid signature one byte at a time. It's a small detail. It's also the difference between "secure" and "secure until someone looks closely."

Access granted confirmation screen after successful single-use code validation

What this actually costs to run

Nothing, at the scale most people need. The whole thing runs on Netlify Functions (serverless — no server to manage) with Netlify Blobs as the storage layer, which is built into a free Netlify account. No database to provision, no monthly hosting bill, no infrastructure to patch.

What nobody tells you: five things that will bite you if you skip them

Every tutorial covers the happy path — code entered, code accepted, done. Here's what actually breaks in production, and what I haven't seen written up anywhere else.

1. Netlify Blobs defaults to eventual consistency, and that quietly reopens the race condition Netlify's own documentation confirms Blobs uses an eventual consistency model by default, with writes guaranteed to propagate to all edge locations within 60 seconds. If your validation read doesn't explicitly request strong consistency, there's a real window — up to a minute — where a just-used code can read back as "not found" from a different edge node. That means it could pass validation a second time. This system requests { consistency: "strong" } specifically on the read that decides whether a code is valid. Drop that one option during a refactor and you've silently reintroduced the exact bug the rest of the design exists to prevent.

2. You're storing more buyer data than most tutorials mention — and that has legal weight Every successful validation logs the buyer's IP address and user-agent alongside the timestamp, for abuse detection and support debugging. That's personal data under GDPR. If you sell to EU customers, "I didn't think about it" isn't a policy. You need an actual answer to how long you keep it and how someone can ask you to delete it — before your first refund dispute forces the question, not after.

3. Nothing here rate-limits guessing attempts — that's on you to add A short, predictable code format is brute-forceable if nothing throttles repeated attempts. This system doesn't include rate limiting on the validation endpoint. Netlify's Firewall Traffic Rules (available on the free tier) or a dedicated rate limiter is something you should add yourself, especially if your codes are short or sequential. Keep the human-readable format for your own support emails (SUMMER-A1B2), but make the random part long enough that guessing isn't realistic — four digits is not that.

4. The admin dashboard's "auth" is a shared secret, not a login — and that's a deliberate tradeoff, not an oversight /admin.html is protected by one secret string, compared in constant time so it can't be guessed via timing, but it's still a single shared password, not individual accounts. Fine for one operator checking their own sales occasionally. Not fine the moment a second person needs access, or you want to know who looked at the codes, not just that someone did. Swap it for real auth before that becomes true.

Access code entry screen for a single-use paywall gate on a digital product

5. "Free" storage isn't a permanent guarantee — check before you plan around it Netlify has moved to credit-based billing, and its own docs currently state that Blobs/Database storage costs haven't been finalized — pricing is deferred, to be announced ahead of a stated date. If you're building a cost model that assumes today's free tier holds at real volume, don't. This is a moving target right now; check Netlify's current pricing before you scale past a side-project's worth of codes, because no article — including this one — can promise you a number that will still be accurate in six months.

Try it yourself

I put a live, working demo online rather than just describing this in the abstract — you can enter a demo code and watch the whole flow, including an admin view showing which codes have been used:

https://access-code-paywall-demo.netlify.app (demo codes: DEMO-0001, DEMO-0002, DEMO-0003 — each works once, so if someone tests it before you, grab whichever one is still available)

If you need this built for your own project

This exact system — server-side validation, fail-closed error handling, atomic single-use enforcement — is something I build for people who need to gate a course, an ebook, or private content without adopting an entire membership platform. If that's you, I take on this kind of work on Fiverr — happy to talk through your specific setup before you commit to anything.

FAQ

What is a single-use access code system? It's a lightweight alternative to a membership platform: a buyer enters a code after purchase, the code is checked and permanently marked as used on the server, and the same code can never unlock content a second time — whether reused by the original buyer or shared with someone else.

Why not just check the code in the browser with JavaScript? Because anyone can open developer tools and read the full list of valid codes directly in the page source. A client-side check can be bypassed entirely; it isn't real security, just an inconvenience.

What does "fail-closed" mean? It means that if any part of the system fails — a storage read times out, a service is briefly unreachable — access is denied by default. The alternative, "fail-open," silently grants access when something breaks, which is the opposite of what you want from a paywall.

Do I need a database for a single-use access code paywall? No. This implementation uses Netlify Functions (serverless) and Netlify Blobs for storage, both included in a free Netlify account — no database to provision or maintain.

Can a single-use code be used twice if two requests arrive at the same time? Not if it's built correctly, but the naive fix is less reliable than it looks. The primary defense is an explicit read-before-write check against strongly-consistent storage: read first, and if the code already has a record, reject it. A second, atomic conditional write acts as a backup layer for the rare case of two truly simultaneous requests — but in my testing, that atomic "write-only-if-new" flag occasionally reported success on a key that already existed, which would have silently broken single-use guarantees if I'd relied on it alone. Lesson: treat your storage layer's atomicity guarantees as a second line of defense, not the only one — verify them yourself under load rather than trusting the docs.

Does Netlify Blobs guarantee strong consistency for a paywall check? Not by default. Netlify Blobs uses eventual consistency out of the box, with up to 60 seconds for writes to propagate across edge locations. A single-use code check needs strong consistency explicitly requested on the read, or there's a real window where a used code can briefly validate again from a different region.

Is storing a buyer's IP address for a single-use code a privacy concern? Yes — an IP address is personal data under GDPR and similar regulations. If a system logs it (commonly done for abuse detection and support debugging), that needs to be reflected in an actual privacy policy, with a real retention and deletion answer, especially for EU buyers.

Can I hire someone to build this exact system for my own project? Yes — this is a system I build for creators and small businesses who need to gate an ebook, course, or private report without adopting a full membership platform. I take on this kind of work on Fiverr; feel free to reach out with your specific setup before committing to anything.


About the author: I'm a Netlify developer focused on serverless access-control systems — access-code paywalls, single-use validation, and fail-closed security logic. This system is running in production; you can test the live demo above and see the admin dashboard tracking real code usage.

Post a Comment

0 Comments