auth · tutorial

JWT Sessions That Bounce You Forever (10-Line Fix)

Login loops and mid-checkout expiries share one cause: session logic split across layers. The single-callback JWT shape that ends both. Inside.

Sailors riding carousel horses at a carnival, 1943
Sailors riding carousel horses at a carnival, 1943 · Photo: Marjory Collins, public domain, via Wikimedia Commons

Logged in, bounced, logged in, bounced

Click login. Consent. Land on the dashboard, which redirects to sign-in, which sends you back.

The infinite bounce.

Or the subtler cousin.

Logged in for weeks, deep into checkout, and the payment step declares you a stranger.

Cart intact.

Session dead.

Buyer gone.

Both share one cause. Session truth split across layers that disagree.

Middleware believes one thing. The page believes another. The token expired underneath both.

Session failures and what they cost

Failure The user sees The business cost
A login redirect loop Bouncing pages, never in Affected logins lost
Expiry mid-checkout "Log in again" Abandoned checkouts
A stale role Paid, treated as free A support ticket, refund risk
A token that never expires Nothing A stolen cookie works forever
Clock-skew rejection Random logouts Unreproducible complaints

Three details make loops and expiries siblings.

Two writers means no truth. Middleware refreshing tokens while a callback mints them produces dueling sessions. Each is valid alone and contradictory together.

Fixed expiry ignores behavior. A 24-hour absolute timeout kills the weekly buyer on day two and keeps a stolen token alive for its full day.

Stateless tokens cannot be deleted. A token signed once stays valid until it expires, wherever it was copied. The defense is to check the user record on every guarded request.

What to do

One place decides what goes into a session. Everything else only reads it.

Put the user's ID into the token and onto the session in a single step. Pages, APIs and guards never mint, refresh or reinterpret it. They read the ID and trust it.

That removes the dueling-sessions cause of loops.

Check the user on every guarded request.

Look the user up by ID each time.

If the session points to someone who no longer exists, treat the visitor as signed out.

If access changes, the next request sees it.

No new login.

Know the expiry you are using.

Sessions are long-lived tokens that expire on a schedule and refresh while the person stays active.

The session drill

Check Action Pass looks like
No loop A fresh login, then the dashboard Lands once, zero bounces
Staying in Come back after a few days Still signed in
Removed user Delete a test user, reuse its session Treated as signed out
Access change Grant or remove paid access The next request reflects it
Checkout span Start a purchase and take your time Completes without a re-login

Run it every quarter with two browsers. Auth libraries change defaults between major versions. The drill re-proves what your config claims.

What the check costs

One indexed read per guarded request. That is the cheapest security in the stack.

Debugging a bounce

Work through the causes in this order.

First, cookie flags that do not match the environment, like a secure cookie on plain HTTP. Second, a site address that differs between environments. Third, a callback writing fields the middleware cannot read.

Most bounces end at one of those three.

What a refresh buys

A session that refreshes while the person is active keeps the weekly buyer signed in.

An idle session expires, so a stolen cookie cannot live forever. Active people stay in. Idle tokens die.

Know this first

A signed token cannot give you instant global logout on its own. BoringPush adds no per-device session list and no revoke-all switch.

The guard checks the user on every protected request. That covers access.

Why BoringPush is the way to do this

Sessions that don't loop.

One place decides what goes into a session. Everything else only reads it. The paid-access guard checks the user every time.

Signed in stays signed in. A removed user is treated as signed out.

FAQ

Do the same ideas work with database sessions?

Yes. One writer, a long expiry that refreshes with activity, and a user check on each guarded request all carry over.

Signed tokens win for solo scale because they need no lookup on every page. Database sessions win when you need an instant per-session revoke list.

Why not just use a longer absolute expiry?

Because it optimizes the wrong end. A very long token keeps a stolen cookie dangerous for a season. A refreshing session keeps active people in and lets idle ones expire.

How do multiple devices stay signed in?

Each device holds its own token. Signing out on one device clears only that token. There is no built-in list of devices in the kit.

What causes the classic dashboard-to-sign-in bounce?

The middleware and the page disagreeing about whether you are signed in. Common causes are cookie flags that do not match the environment, a mismatched site address between environments, and a callback writing fields the middleware cannot read. Check them in that order.

Should checkout extend sessions automatically?

Authenticated requests refresh the session as they happen, and checkout is a burst of them. No special case is needed, which is the point.

When do I need rotating refresh tokens?

With compliance requirements such as finance or health data, or with long-lived API clients and mobile apps. A web SaaS with human buyers rarely needs that machinery. Adopt it when an auditor asks, not when a blog post does.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading