Skip to main content

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.

PackageUse it in
@zyphr-dev/auth-reactReact web apps
@zyphr-dev/auth-react-nativeReact Native / Expo apps
@zyphr-dev/auth-coreNon-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>

PropTypeNotes
clientZyphrClientConstructed with your publishable application key.
storageTokenStorageWhere tokens persist. Web defaults to localStorage; RN defaults to in-memory (pass a secure store — see below).
refreshSkewMsnumberRefresh this many ms before access-token expiry. Default 60000.
refreshOnForegroundbooleanReact 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:

statusMeaningisAuthenticated
loadingReading persisted tokens on startupfalse
authenticatedSigned in, token validtrue
refreshingA proactive refresh is in flight — still signed intrue
unauthenticatedSigned outfalse

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 localStorage with an in-memory fallback. localStorage is 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 an httpOnly cookie.

  • React Native (Expo) — pass secureStoreAdapter(SecureStore) (encrypted; recommended) or asyncStorageAdapter(AsyncStorage) (not encrypted — use only if you already standardize on it).

  • React Native (bare, no Expo) — pass keychainAdapter(Keychain) over react-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.

warning

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));