Skip to main content

What You’ll Build

A production-grade Token management layer — covering persistence storage selection, multi-user isolation, concurrent refresh race conditions, expiry degradation, proactive revocation, and secure storage. This ensures your SaaS application works correctly even after Token rotation and service restarts.

Token Lifecycle

Core Principles

Violating any of the following will cause users to silently lose their session — no error message, just a sudden “please log in again.”
1. Always persist after refresh — The onTokenRefresh callback is your only chance to save the new Token. Miss it, and the old Refresh Token is already invalidated while the new Token is lost — the user must re-authorize. 2. Never use in-memory-only storage — Service restart = all user Tokens lost = everyone must log in again. Tokens must be persisted to disk (DB / Redis / file). 3. Rotation must be atomic — After each refreshToken() call, the old Access Token and Refresh Token are both invalidated. Saving the new Token and discarding the old one must happen in the same transaction.

Step 1: Choose a Storage Strategy

For 80% of use cases, the Contrib DrizzleTokenStore or SQLAlchemyTokenStore will work. You only need a custom implementation for special requirements (Redis sessions, custom encryption).

Using Contrib Storage

Using Redis Custom Storage

Step 2: Multi-User Token Management

SaaS applications typically maintain independent Tokens for each user. The core pattern: use userId as the key, isolate reads and writes.

Step 3: Concurrent Refresh Race Conditions

Problem: Two requests simultaneously discover the Access Token is expired and both attempt to refresh. The first succeeds, but the second receives invalid_grant because the Refresh Token has already been rotated. Solution: Use a mutex to ensure only one refresh operation runs at a time per user.
The Python version re-checks if the Token was already refreshed by another coroutine after acquiring the lock (double-check pattern), avoiding redundant refreshes. The TypeScript version achieves the same effect by sharing a single Promise.
For multi-instance deployments (multiple pods / processes), in-process Map / dict locks aren’t sufficient. You need a distributed lock:

Step 4: Token Expiry Degradation

When the Refresh Token also expires (90 days without use) or is revoked, the SDK throws AuthExpired. The only recovery path is guiding the user to re-authorize.
Frontend handling of the auth_expired response:
Frontend Degradation Component

Step 5: Token Revocation

When users proactively disconnect (revoke authorization) or you need to clean up unused Tokens, call the revocation API.
After revocation, the user’s Access Token and Refresh Token are immediately invalidated. Subsequent API calls will receive 401. Make sure all in-flight requests are completed before revoking.
Typical revocation triggers:
  • User clicks “Disconnect Profy” in your App settings
  • User deletes their account
  • Admin bulk-cleans inactive authorizations
  • Abnormal access patterns detected

Step 6: Secure Storage

Encryption at Rest

Tokens are equivalent to user credentials and should be stored encrypted in the database:

Security Checklist

Anti-Patterns

The following are common Token management mistakes in production environments — each one has caused real production incidents.

1. Storing Tokens in Frontend localStorage

Tokens should only exist on the server side. The frontend references them via HttpOnly cookies or Session IDs.

2. Ignoring the onTokenRefresh Callback

After refresh, the old Refresh Token is already invalidated. If the new Token isn’t saved, the next refresh will fail and the user will be forced to re-authorize.

3. Using an Already-Rotated Refresh Token

Always read the latest Token from persistent storage — don’t rely on in-memory caches.

4. Not Handling Concurrent Refreshes

Refer to the mutex pattern in Step 3.

Production Checklist

Verify each item before going live:
  • onTokenRefresh is configured and writes to persistent storage
  • Tokens are stored server-side (DB / Redis), not on the frontend
  • Tokens are encrypted at the storage layer
  • Concurrent refreshes are protected with a mutex
  • AuthExpired has degradation handling (guide to re-authorize)
  • revokeToken() is called when users disconnect
  • Logs do not contain full Token values
  • OAuth scopes follow least privilege
  • Multi-instance deployments use distributed locks
  • Encryption keys are not hardcoded in source code

Next Steps

Next.js Full-Stack Integration

Build a complete OAuth + billing Next.js application from scratch

Python FastAPI Integration

Python SDK + SQLAlchemy Token persistence

Webhook Event Handling

Receive and process real-time events pushed by the Profy platform

SDK Complete Guide

Dual-language API reference and Token persistence