Auth Session (React & React Native)
Drop a provider into your app and get a reactive auth session with automatic, proactive token refresh — no token-lifecycle plumbing to write. Same API on the web and on React Native.
<ZyphrAuthProvider>— provides the session to your component tree.useSession()— reactive{ status, accessToken, isAuthenticated }.useAuth()—login,logout,refresh.
These packages build on the client-safe ZyphrClient
(publishable application key — no secret in the client) and share a
framework-agnostic engine, @zyphr-dev/auth-core.
| Package | Use it in |
|---|---|
@zyphr-dev/auth-react | React web apps |
@zyphr-dev/auth-react-native | React Native / Expo apps |
@zyphr-dev/auth-core | Non-React runtimes, or a custom integration |
React (web)
npm i @zyphr-dev/auth-react @zyphr-dev/node-sdk react
import { ZyphrClient } from '@zyphr-dev/node-sdk';
import { ZyphrAuthProvider, useSession, useAuth } from '@zyphr-dev/auth-react';
const client = new ZyphrClient({ applicationKey: 'za_pub_xxx' });
function App() {
return (
<ZyphrAuthProvider client={client}>
<Screen />
</ZyphrAuthProvider>
);
}
function Screen() {
const { status, isAuthenticated, accessToken } = useSession();
const { login, logout } = useAuth();
if (status === 'loading') return <p>Loading…</p>;
if (!isAuthenticated) {
return <button onClick={() => login({ email, password })}>Sign in</button>;
}
return <button onClick={() => logout()}>Sign out</button>;
}
The web package stores tokens in localStorage by default, with an in-memory
fallback for SSR. See Token storage for the security tradeoffs
and how to override it.
React Native
npm i @zyphr-dev/auth-react-native @zyphr-dev/node-sdk
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
import { ZyphrClient } from '@zyphr-dev/node-sdk';
import {
ZyphrAuthProvider,
useSession,
useAuth,
secureStoreAdapter,
} from '@zyphr-dev/auth-react-native';
const client = new ZyphrClient({ applicationKey: 'za_pub_xxx' });
export default function App() {
return (
<ZyphrAuthProvider client={client} storage={secureStoreAdapter(SecureStore)}>
<Screen />
</ZyphrAuthProvider>
);
}
useSession() and useAuth() are identical to the web binding. The React Native
provider also refreshes when the app returns to the foreground — a
backgrounded app's scheduled refresh timer may not fire reliably, so this keeps
the token fresh on resume. Disable with refreshOnForeground={false}.
API
<ZyphrAuthProvider>
| Prop | Type | Notes |
|---|---|---|
client | ZyphrClient | Constructed with your publishable application key. |
storage | TokenStorage | Where tokens persist. Web defaults to localStorage; RN defaults to in-memory (pass a secure store — see below). |
refreshSkewMs | number | Refresh this many ms before access-token expiry. Default 60000. |
refreshOnForeground | boolean | React Native only. Refresh on app resume. Default true. |
useSession()
Returns reactive session state; re-renders on every change (login, logout, refresh):
const { status, accessToken, isAuthenticated } = useSession();
Check isAuthenticated, not status, to decide "is the user signed in."
status has four values, and one of them is a trap:
status | Meaning | isAuthenticated |
|---|---|---|
loading | Reading persisted tokens on startup | false |
authenticated | Signed in, token valid | true |
refreshing | A proactive refresh is in flight — still signed in | true |
unauthenticated | Signed out | false |
refreshing is an authenticated state: the proactive refresh fires ~60s
before the token expires (refreshSkewMs), so the session is valid throughout.
isAuthenticated (and accessToken) stay put across it. A naive
status === 'authenticated' check would read as signed out every time a
background refresh runs — roughly once per token lifetime, intermittent and hard
to reproduce. Use isAuthenticated:
const { isAuthenticated, status } = useSession();
if (status === 'loading') return <Splash />; // gate cold-start on this
if (!isAuthenticated) return <LoginScreen />; // NOT status === 'unauthenticated'
return <App />;
Use accessToken for authenticated API calls (e.g. via zyphr.authedRequest());
it remains available during refreshing.
useAuth()
const { login, logout, refresh } = useAuth();
await login({ email, password });
await logout();
await refresh(); // usually automatic — the provider refreshes before expiry
Proactive token refresh
The provider refreshes the access token shortly before it expires
(refreshSkewMs, default 60s), so requests never race an expiry. Because the
refresh happens while the current token is still valid, the session stays
authenticated throughout (status: 'refreshing', isAuthenticated: true) —
see useSession(). Concurrent refreshes are de-duplicated into a
single call, and only if the refresh token is rejected does the session
transition to unauthenticated (a clean logout). On React Native it additionally
refreshes on foreground.
Token storage
Tokens persist through a TokenStorage interface, so you control where they live.
-
Web — defaults to
localStoragewith an in-memory fallback.localStorageis readable by any script on the page, so it's exposed to XSS token theft. For the strongest posture, proxy auth through your own backend and keep the refresh token in anhttpOnlycookie. -
React Native (Expo) — pass
secureStoreAdapter(SecureStore)(encrypted; recommended) orasyncStorageAdapter(AsyncStorage)(not encrypted — use only if you already standardize on it). -
React Native (bare, no Expo) — pass
keychainAdapter(Keychain)overreact-native-keychain(iOS Keychain / Android Keystore, encrypted):import * as Keychain from 'react-native-keychain';
<ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} />
All of these are optional peer dependencies — install only the one you use.
If you supply no storage, the provider falls back to in-memory storage
and the session is lost on every reload / app restart. In development you'll see
a console.warn about this. Always pass a persistent store in production.
Supply any store by implementing the interface (sync or async — the core always awaits it):
import type { TokenStorage } from '@zyphr-dev/auth-react'; // or auth-react-native
const storage: TokenStorage = {
getItem: (key) => /* string | null | Promise<…> */,
setItem: (key, value) => /* void | Promise<void> */,
removeItem: (key) => /* void | Promise<void> */,
};
Multiple accounts on one device (keyPrefix)
By default the session persists under a single key, zyphr.session. To keep
several accounts signed in at once (account switching), give each provider a
distinct keyPrefix so their tokens don't collide in a shared store:
<ZyphrAuthProvider client={client} storage={keychainAdapter(Keychain)} keyPrefix={`${accountId}:`}>
{/* ... */}
</ZyphrAuthProvider>
The effective storage key becomes ${keyPrefix}zyphr.session (default '', so
existing single-session apps are unchanged). Render the provider for the active
account and key it by accountId so switching remounts cleanly.
The engine: @zyphr-dev/auth-core
Both bindings wrap a framework-agnostic SessionManager from
@zyphr-dev/auth-core — the session state machine, refresh scheduler, and storage
interface. Use it directly in a non-React runtime, or to integrate a custom
transport (for example, proxying auth through your own backend):
import { SessionManager, type AuthTransport } from '@zyphr-dev/auth-core';
const transport: AuthTransport = {
async login({ email, password }) { /* return { accessToken, refreshToken, expiresAt } */ },
async refresh(refreshToken) { /* return fresh SessionTokens; throw to log out */ },
async logout(refreshToken) { /* revoke the refresh token */ },
};
const session = new SessionManager({ transport });
await session.initialize();
session.subscribe((s) => console.log(s.status));
Related
- Client-side Auth — the
ZyphrClientthese packages build on. - Managing End Users — server-side user administration.