Skip to content
← Work index

05Case study

Paywallio

Content monetisation and gated access

Year
2026
Role
Full-stack
Status
Live

A paywall is one question asked constantly: may this person see this? Get it wrong in one direction and you give away the product. Get it wrong in the other and you query the subscriptions table on every asset on every page.

JWT
Entitlement travels with the request
Postgres
Single source of subscription truth
Tiered
Plan-scoped content access

The constraint

What made it hard

The naive paywall checks the database on every request, which puts a subscriptions lookup in the hot path of every image, article and API call. The naive fix - a long-lived session flag - is worse: a cancelled subscriber keeps their access until the flag expires, and revocation becomes something you hope happens rather than something you can point at.

Approach

  1. 01

    The claim carries the plan

    Access rights are minted into a signed token when the subscription state is read. Verifying it is a signature check, not a query, so the common case - an active subscriber loading a page - costs no database time at all.

  2. 02

    Short expiry is the revocation mechanism

    Tokens are deliberately short-lived. Cancelling does not need to reach out and invalidate anything: the claim simply stops being reissued, and access ends within one refresh window. Revocation becomes a property of the design rather than a cleanup job.

  3. 03

    Billing events are the only writer

    Subscription state changes arrive as webhooks and are written in one place. Nothing else in the system is allowed to decide that someone is a subscriber, so there is exactly one story about why a given user has access.

  4. 04

    Usage recorded as an append-only history

    Consumption is logged rather than counted in place, which makes the analytics dashboard a read over history and makes any billing dispute answerable by replaying what actually happened.

Architecture

Paywallio entitlement checkReadercarries a tokenEntitlement checkverify · no db readContentdeliveredPaywallupgrade pathBilling eventswebhookPostgreSQLplans · expiry · usagerequest + claimentitledexpired or absentmints the next claimthe claim proves the plan on its own · the database decides when it stops being true
The check on the left runs on every request and touches no database. The row below decides what the next claim will say.

Specification

Entitlement
Signed JWT claims, short TTL
Revocation
Expiry-driven - no invalidation sweep
State
PostgreSQL via Prisma, webhook-written
Usage
Append-only log behind the analytics view
Runtime
Next.js 16, React 19, TypeScript end to end

Stack

  • Next.js 16
  • React 19
  • TypeScript
  • PostgreSQL
  • Prisma
  • JWT
  • Tailwind CSS

Where it landed

  • 01

    The hot path for a paying reader is a signature verification, not a query.

  • 02

    Cancellations take effect within a refresh window without a background job.

  • 03

    Every access decision can be traced to one subscription record and one billing event.

Next case study

06

GetFreeXRP

Crypto rewards priced against a moving market

Read →

Something that has towork in production?

I take on contract engineering and full-time roles. Most useful first message: what you are building, what is currently in the way, and when you need it live.