How to implement secure, long-lived sessions with short-lived access tokens and refresh tokens — the pattern used by Google, GitHub, and Stripe.
If you set your JWT expiry to 7 days and that token is stolen, the attacker has 7 days of access. You can't revoke it without a deny-list (which requires a DB lookup on every request).
Use two tokens:
| Token | Lifespan | Storage | Purpose |
|---|---|---|---|
| Access token | 15 minutes | Memory / cookie | API authorization |
| Refresh token | 30 days | httpOnly cookie + DB | Get a new access token |
When the access token expires, the client silently exchanges the refresh token for a new one — the user never sees a login prompt.
Every time a refresh token is used, issue a new refresh token and invalidate the old one. If you detect reuse of an old token, it means it was stolen — immediately revoke the entire token family.
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT UNIQUE NOT NULL,
family_id UUID NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN DEFAULT false
);
To log out all devices: delete all refresh tokens for the user from the DB. Every existing session will fail at the next refresh cycle.