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 on_refresh callback on reportEvent (or saving manually after calling refresh()) is your only chance to persist 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 refresh() 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

ProfyApp does not include built-in Token storage — it handles OAuth handshake and API calls only. Storage is your application’s responsibility, ensuring maximum flexibility.

Initialize ProfyApp + Storage

ProfyApp only holds App identity, not user Tokens. Tokens are passed per API call, and rotated tokens are persisted via the onRefresh callback.

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.
revoke() takes a refresh token (not an access token). Access tokens are stateless JWTs and cannot be revoked; revoking the refresh token prevents further refreshes.

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, refresh() throws AuthenticationError (401). 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.
revoke() takes a refresh token. After revocation, the user’s Access Token (expires naturally within 1 hour) 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.

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 Refresh Persistence

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:
  • Token refresh is followed by immediate persistence (onRefresh callback or manual save after refresh())
  • 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
  • AuthenticationError has degradation handling (guide to re-authorize)
  • revoke(refreshToken) 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

Per-Use Billing

Implement per-call billing with ProfyApp reportEvent

Next.js Full-Stack Integration

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

Python FastAPI Integration

Python SDK + Token persistence

SDK Complete Guide

Dual-language API reference