Skip to content

How I wrote a stateless gift link feature

Published: at 04:45 PM

Last year the company currently I work for, Scaled Agile, decided to put the majority of their IP content behind a content gate. Regardless of how you might feel about that decision, I’d like to write about the technical solution myself and my team used to accomplish a gift link feature, much like you’d see at an excellent journalism website like Defector. The process of building this feature serves as something like an excellent case study in feature development and technical tradeoffs

The basics of the content gate requirements are pretty simple:

Only users with a paid account should be able to read full articles. Everyone else sees a snippet of the article with a call to action to register and log in (aka a content gate)

Once we implemented this feature, we immediately got request from internal sales teams, some customers, and external partners requesting full access to the site. Short of building a new license model which takes months of preparation, I proposed implementing a gift link feature so that authenticated users could gift a specific article to someone else. I also added a sitewide flag so that internal users can grant access to the entire site to the recipient. While this was not requested, as is the way of software development, it quickly took on a life of its own and is used frequently to provide access to customers until our new license model is ready.

Here is the set of requirements I gave myself while building this feature which I workshopped with the interested stakeholders, including program managers and executives:

Article Gift Links Requirements

A Stateful Approach

One way to solve this would be to use persistence (i.e., a database) to track each gift link, its accessors, and duration. The tables might look something like this:

Gift Link
---
id (pk) - UUIDv4
expiresAt - timestamp
articlePath - string
createdBy - string
isActive - boolean (default true)

We would create a record in this table each time a user creates a gift link. The gift link would look something like this:

https://framework.scaledagile.com/gift-link?id=<id>

where the id is the globally unique (and unguessable) ID of the record.

When a gift link recipient accesses their gift link, we would read this table and check to see if the record is not expired and active. If so, the user would receive the full article content.

A Stateless Approach

The other way to solve this is without persistence, effectively using a JWT to provide stateless authentication. This is the approach that we ended up using. Here’s how it works:

  1. When an authenticated user visits a gift-linkable page, the server generates a gift link on page load
  2. The gift link looks like https://framework.scaledagile.com/gift-link?token=<token>&sig=<sig>
  3. The token is a JSON token that is base64 and URL encoded which looks like this when decoded:
{"article_url":"somearticlepath","created_by":"mikepray","expires":1743263195}
  1. I should have followed the JWT spec more closely, but nevertheless…
  2. The gift link is signed with a HMAC-SHA256 hash using a private key known only to the server

When a gift link recipient access this link, the server intercepts the request and validates the token by re-hashing the token with the private key. If the token hash matches the signature and the token is not expired, then the server grants access to the article. Then the server then puts a cookie on the users’ browser with the token and signature. On each pageview the server validates the token and checks the expiration.

Tradeoffs

Both methods are secure and prevent abuse by preventing users from building or guessing a valid gift link. The stateful approach prevents this by keeping a record of gift links and using unguessable UUIDs. The stateless approach prevents this by signing the token with a private key known only to the server so that users can’t grant themselves perpetual access to the entire site. Both approaches are fairly easy to administer, with the default expiration being stored in the application DB and adjustable per gift link (and even per-article).

The stateful approach has the drawback of requiring a database and frequent reads. In practice for most web applications this is not a showstopper, but it does add some latency and adds another link in the chain which requires debugging and maintenance when things go wrong.

The stateless approach requires no persistence and the only latency introduced is the computation of the signature on page read. Enabling this feature requires no dependencies, no DB migrations, and is not likely to break on other dependency upgrades.

The major drawback to the stateless approach is that once gift links are generated and sent the expiration cannot be modified. Gift links can be invalidated by rotating the private key, but this invalidates all gift links, so there is no way to invalidate a specific link without introducing persistence. If there’s a way to invalidate individual links without keeping state, please drop me a line.

We ran up against this tradeoff quickly. There was a bug in the expiration code that required re-creating some links for certain users and a manual process of notifying them.

The other drawback is that we only track usage based on expiration. If there had been a requirement to allow only n number of accesses, then a persistence approach would be required.

Tracking

We use PostHog as our marketing and analytics platform and our server sends events to PostHog using backend events every time a user accesses a gift link. This is required because a cookie is used to validate gift links after the initial gift link access. I love PostHog and the dashboards we create show gift link usage in the dozens of thousands in the last 30 days. We can use these dashboards to track abuse of gift links (meaning, users posting gift links on Reddit).

Summary

If I was to build this feature again, I would probably use a database. Being able to invalidate or extend the expiration of individual links (especially for the case of our large customers using sitewide links) is enough of a boon that persistence is probably the safest route despite the dependency of additional DB reads on page load. That being said, the stateless approach is very robust and our solution as shaped to the requirements serves as a good case for a pragmatic technical solution.


Previous Post
Coding Isn't the Hard Part
Next Post
How I think about datetime in systems engineering