Authentication is the system most likely to be rebuilt in year two, and the rebuild is almost always caused by three decisions made in the first weekend. This is the setup that survives: magic links, short-lived access tokens, a server-side session record, and no password field anywhere.

The Shape of It

A user submits an email. You create a single-use token, store its hash with a 15-minute expiry, and email the link. On click you verify the hash, delete the token row, create a session row, then set two things: an access token with a 15-minute lifetime in memory or a short-lived cookie, and a refresh token in an httpOnly, secure, SameSite=Lax cookie scoped to your API path.

That is four database tables total: users, login_tokens, sessions, and whatever you call your audit log. If your auth design needs more tables than that before you have customers, it is doing something you have not justified yet.

The Three Decisions That Cause Rebuilds

First: putting the refresh token in localStorage. Any script on your page can read it, and you cannot revoke what you cannot see. Use an httpOnly cookie and accept that you now need CSRF protection on state-changing routes, which is one header check.

Second: stateless sessions with no server record. A pure JWT design means logout is a lie and a compromised token stays valid until expiry. Keep a sessions row with a revoked_at column; check it on refresh, not on every request. You get real revocation for one extra query per 15 minutes per user.

Third: encoding permissions into the token. The moment a plan changes or a teammate is removed, every issued token is stale. Put the user id and session id in the token and nothing else. Read roles and entitlement from the database in the request, where they are current.

Rate Limits That Belong Here

Magic links collapse three attack surfaces into one endpoint, so protect that one endpoint properly. Limit login requests to 5 per email address per 15 minutes and 20 per IP per 15 minutes, counted in a durable store rather than in process memory. Always return the same response whether or not the address exists, because a different response is an account-enumeration feature you did not intend to ship.

Token verification needs its own limit: 10 attempts per token hash, then invalidate. Without it, a leaked link in a shared inbox can be brute-forced on the trailing characters.

The Parts Worth Adding Early

Two additions cost an hour each and save days later. An audit log row for every login, logout, and refresh-token rotation, with IP and user agent, is the only thing that answers “was this account accessed” when a customer asks. And refresh-token rotation with reuse detection: issue a new refresh token on every use, and if an old one is presented, revoke the whole session family. That single rule turns a stolen cookie from an open door into a one-time event you can see.

What to Skip Until Someone Pays For It

Skip social login until a real user asks; it adds provider outages to your auth path. Skip TOTP two-factor until you have a paying customer who requests it, then add it in front of the login-token step rather than after session creation. Skip SAML entirely until an enterprise contract funds the week it takes.

Weekend Checklist

  • Login tokens are stored hashed, expire in 15 minutes, and are deleted on first successful use.
  • Access token lifetime is 15 minutes or less and carries only a user id and session id.
  • Refresh token is in an httpOnly, secure, SameSite cookie scoped to the API path, never in localStorage.
  • A sessions table exists with revoked_at, and refresh checks it before issuing a new access token.
  • Refresh tokens rotate on every use, and presenting a used token revokes the entire session family.
  • Login is limited to 5 requests per email and 20 per IP per 15 minutes, in a durable counter.
  • The login response is identical for existing and unknown email addresses.
  • Roles and plan entitlement are read from the database per request, never from the token payload.
  • Login, logout, and rotation events are written to an audit table with IP and user agent.
  • You can revoke one session and all sessions for a user from a single SQL statement you have actually run.

Do this in a weekend and auth becomes the part of the system you stop thinking about. Skip the sessions table and the rotation rule and you will rebuild all of it the first time a customer forwards a login email to a colleague.

Get articles like this by email

Production patterns, checklists, and failure stories for people shipping SaaS. No spam.