Auto-Login Email Links: From Naive Implementation to Stateless Tokens
Our sites were sending around 600,000 emails daily. The marketing team wanted every link in those emails to auto-login users — so that if someone didn't have an active session on the website, clicking a link would log them in seamlessly.
We have both websites and mobile apps, so it's entirely possible that a user creates an account on desktop, then receives an email on their phone and opens a link there.
The Naive Implementation
My first approach was straightforward. I created a force_login_tokens collection with two fields:
_id— a UUIDv4 token combined with an encrypted value using a unique salt per userdate— the creation timestamp, so tokens could expire via MongoDB's TTL index
Each email needed at least one token (all links in the same email could share one), but tokens had to differ between emails because expiration was configurable per campaign.
The "Never Expire" Curveball
Marketing asked us to make these tokens never expire. If a user opens an email from a year ago — or longer — the link should still work.
I pushed back, citing security concerns and doubting anyone would open emails that old. But they had data showing that some users do revisit old emails, and occasionally it leads to an upgrade — which means revenue. Hard to argue with that.
The Problem: Uncontrolled Growth
With ~600,000 emails per day, the force_login_tokens collection was growing fast. Rough math: 219 million tokens per year, over a billion in five years. No expiration meant nothing was being cleaned up. This ran fine for a few years until I noticed this small feature was consuming far more resources than it should. The collection had grown to around 500 million records.
Finding a Better Solution
When I finally had time to address this technical debt, I considered two approaches:
Option 1: One token per user per day. Reuse the same token across all emails sent to a user on a given day. This works well enough because expiration doesn't need granularity below the day level. A solid improvement, but still requires database storage.
Option 2: Self-contained tokens. Instead of storing tokens in the database, encode all the login information into the token itself — similar in concept to a JWT.
I went with Option 2:
const forceLoginToken = Buffer.from(JSON.stringify({
uid: userId,
date: moment().unix(),
signature_type: 'sha256',
signature: await this.generateForceLoginTokenSignature(userId, now, 'sha256'),
})).toString('base64');
The signature is generated from the user ID, the creation timestamp, and a secret salt unique to each user. The resulting Base64 string becomes the force_login_token query parameter on every link in the email.
When a user clicks the link, we decode the Base64, parse the JSON, and regenerate the signature using the same inputs plus the user's secret salt. If it matches the signature field, we know the data hasn't been tampered with and the uid genuinely belongs to someone with access to that email inbox.
The Result
- Zero token storage in the database. The entire collection was dropped.
- No index recalculations on every insert.
- No wasted storage or RAM on an ever-growing collection.
- No database queries to validate a token — it's all computed on the fly.
- Flexible expiration — each token carries its creation date, so we can enforce TTLs whenever we choose.