Why We Chose Passwordless Authentication
PIFster is a charitable giving platform. Our users aren't logging in every day — they visit once a month to vote for their favorite charity, maybe donate, and leave. Asking them to remember a password for a site they use twelve times a year is a friction machine.
We watched the support tickets:
- “I forgot my password” — over and over
- Users abandoning the donation flow when the login wall appeared
- Password reset emails that looked identical to phishing
Magic links solve all three. The user enters their email, gets a link, clicks it, and they're in. No password field. No “forgot password” page. No friction.
How Magic Links Work (The 30-Second Version)
2. Server creates a single-use token, stores it with a TTL
3. Server emails a URL containing that token
4. User clicks the link
5. Server validates the token (exists? expired? already used?)
6. If valid: log the user in and redirect. If not: show an error.
That's it. The token is the credential. Once used, it's dead. Once expired, it's dead. No password ever touches the wire.
The Architecture
Our implementation has four pieces:
- REST endpoint — receives the email, creates the token, sends the email
- Token storage — database table with TTL and single-use enforcement
- Email service — sends the magic link with context-aware content
- Consume handler — validates the token and logs the user in
Piece 1: The REST Endpoint
When a user clicks “Send me a sign-in link,” the frontend POSTs to a REST route. The server validates the email, checks rate limits, creates a token, and fires off the email.
Piece 2: Token Storage
Tokens need three properties: single-use, time-limited, and atomically consumed. That last one matters — if two requests hit the same token simultaneously, only one should succeed.
We use a dedicated database table:
The meta column is a JSON blob for anything you need downstream — redirect URLs, action context, feature flags. We store the user's intended action (vote, redeem gift card, etc.) here so the consume handler knows what to do after login.
Token generation is straightforward — just random bytes:
Piece 3: The Consume Handler
When the user clicks the link, WordPress routes it to your handler. The handler validates the token, marks it used (atomically), logs the user in, and redirects.
The atomic consumption is the key security piece. SELECT ... FOR UPDATE locks the row until the transaction commits. If two requests race, one gets the lock and consumes the token; the other sees used_at is already set and bails. No double-logins, no replay attacks.
Security Layers We Added
Magic links shift your attack surface from “guess the password” to “intercept the email.” That's actually a better trade in most cases, but you still need defense in depth.
- 15-minute TTL — tokens expire fast. No “I'll click this tomorrow.”
- Single-use enforcement — atomic DB transaction prevents replay.
- Rate limiting — 3 requests per email per 15 minutes, 12 per day. Per-IP limits too.
- Cloudflare Turnstile — bot protection on the request endpoint. Invisible to real users, blocks automated abuse.
- Origin validation — the REST endpoint checks the HTTP Origin/Referer header against an allowlist.
- Redirect sanitization — the
redirect_toURL in the token metadata is validated against your site's domain. No open redirects.
The Trick That Made It Really Useful: Purpose Routing
Here's where our implementation goes beyond basic magic links. Every token carries a purpose — an enum that tells the consume handler why the user is authenticating:
Each purpose has its own handler that decides what happens after authentication:
- Vote intent — log in and redirect to the voting page with the charity pre-selected
- Gift card redeem — log in and immediately start the redemption flow
- Post-donation — log in and show a “thank you” page with donation details
- Registration verify — confirm the email and finish account creation
The consume handler uses a strategy pattern — a registry of purpose handlers:
This means adding a new “reason to authenticate” is one small class. No if/else chains growing forever.
Context-Aware Emails
The purpose also drives the email content. A magic link for voting gets a different subject line and body than one for gift card redemption:
- Vote: “PIFster sign-in link to vote for Habitat for Humanity”
- Gift card: “PIFster - Redeem your gift card”
- Post-donation: “PIFster Thanks You - Access Your Account”
- New user: “PIFster - Welcome! Access Your Account”
Same infrastructure, different message. The user sees a relevant email instead of a generic “click here to sign in.” This matters for open rates and trust.
What About Users Who Want a Password?
We still support traditional passwords as an option. After authenticating via magic link, users can set a password in their profile if they prefer. But most don't. Our data shows fewer than 10% of users bother setting one — and the ones who do still use magic links half the time.
The key insight: make magic links the default, not an alternative. If you offer both equally, users will choose the password (because it's familiar), hate it (because they forget it), and then ask for a reset link — which is just a worse magic link.
Tips If You Build This
- Start with transients, graduate to a table. WordPress transients (
set_transient) work for prototyping. But for production, you want a real table with proper indexes andFOR UPDATElocking. - Always return 200/201 for unknown emails. Leaking “this email isn't registered” is an information disclosure vulnerability. Send the same response regardless.
- Keep TTLs short. 15 minutes is generous. Some implementations use 5 minutes. The shorter the window, the smaller the attack surface.
- Purge expired tokens. Run a scheduled cleanup (WordPress cron or a simple
DELETE WHERE expires_at < NOW()) to keep the table from growing forever. - Log token consumption, not token values. Log the token hash, the email hash, the purpose, and the outcome. Never log the raw token — it's a credential.
- Add Turnstile or reCAPTCHA. The magic link request endpoint is unauthenticated by definition. Without bot protection, someone can spam emails to arbitrary addresses using your mail server. Cloudflare Turnstile is free and invisible to real users.
PIFster is a community-driven charitable giving platform where donors vote on which charity receives the community's pooled donations each month. We run on WordPress with pay-what-you-want checkout, gift memberships for donor acquisition, and a gamified referral engine. If you're building something similar, we'd love to hear about it.

