How auth-client works
auth-client is a small session machine with pluggable ports. The
key parts:
Core entities
AuthClientfacade — the public API. Composes the ports intologinWithPassword,loginWithSso,refresh,logout,authenticatedRequest, and the event bus.TokenStoreport — persists access + refresh tokens. Default:LocalStorageTokenStore. SSR-friendly alternative:MemoryTokenStore.Transportport — the HTTP layer. Default:FetchTransport. Swap in custom retry, instrumentation, or a mock for tests.Storageport — small KV used for PKCE state, SSO in-flight flags, etc. Default:LocalStorageStorage/MemoryStorage.Cryptoport — Web Crypto adapter for PKCE S256 derivation + random verifier.Clockport —SystemClock/FixedClock. Mostly for tests.Loggerport —ConsoleLogger/NoOpLogger.Broadcastport —BroadcastChannelAdapter(default) /NoOpBroadcast. Cross-tab session sync.
Bootstrap state machine
createAuthClient({ bootstrap }) runs one of three modes at startup:
auto— validates the cached access token against/auth/me. Useful for the common case: refresh has happened in another tab, the user’s role changed, etc. Slight cost on every page load.lazy— trusts the cached claims. Faster boot; first failing request triggers a refresh / re-auth.offline— inert. Read methods return null/false; flow methods throwOfflineModeError. Useful for static demos, Storybook, styleguides.
Every adapter gates first render on auth.ready() so consumers don’t
flash a logged-out UI for a logged-in user.
Refresh-mutex coalescing
When N concurrent requests arrive just-before-expiry:
Request A → 401 → refresh() →Request B → 401 → awaits in-flight refresh →Request C → 401 → awaits in-flight refresh → ↓ single /auth/refresh call ↓ retry A, B, C with new tokenThis is critical because the auth-server’s family-aware reuse detector (RFC 6819 §5.2.2.3) revokes the entire family if it sees a refresh token presented twice. Without the mutex, N parallel refresh calls would trigger N family-revokes — the user would be logged out.
Cross-tab sync (BroadcastChannel)
Every loginWithPassword / logout / refresh emits a message on the
vendidit-auth channel. Other tabs listen and update their internal
state without hitting the server. This means:
- Log in tab A → tab B sees the user immediately.
- Log out tab A → tab B drops the user immediately.
- Refresh-rotation in tab A → tab B picks up the new tokens without
a duplicate
/auth/refreshcall.
Where to plug in custom adapters
| Port | When you’d swap | Example |
|---|---|---|
TokenStore | SSR; HttpOnly cookies; encrypted at rest | new MemoryTokenStore() for SSR; a custom adapter that POSTs to /api/auth/set-cookie |
Transport | Custom retry policy; instrumentation; mock for tests | Wrap FetchTransport with retry-after handling |
Storage | SSR; encrypted storage | new MemoryStorage() |
Broadcast | Worker / non-browser contexts | new NoOpBroadcast() |
Crypto | Node test runs without Web Crypto | Polyfill |
Logger | Production noise control | new NoOpLogger() |
Error model
Every flow method throws a typed AuthError subclass:
UnauthenticatedError— current session is invalid; needs login.ValidationError— input validation failed (withfieldinfo).ConflictError— e.g. email already registered.NotFoundError,ForbiddenError,RateLimitedError,ServerError,NetworkError.RequiresTwoFactorError— login succeeded but 2FA code is required.OfflineModeError— bootstrap isoffline.
The fromHttpResponse(response) helper maps any HTTP response from the
auth-server into the matching AuthError subclass. Adapters surface
these directly so consumer UI can try/catch per type.
Read next
- Quickstart — install + 30-line setup.
- Framework adapters.
- Class reference — auto-generated.
- Playground — live demo.