@tuwaio/siwx-react
@tuwaio/siwx-react is the React Layer 2 (L2) package of SIWX (Sign-In With X), the authentication project of TUWA Stage 1 (“Core Auth & Primitives”, next to Orbit Utils). Built on @tuwaio/siwx-core, react and zustand (with the immer middleware), it runs the CAIP-122 sign-in flow in React apps with two hooks and a session store. It ships no UI components and makes no network requests of its own: you pass the wallet signer and the calls to your backend.
🏛️ Core Capabilities
- Sign-in flow:
useSiwx().signIngets a nonce, builds the CAIP-122 message, asks the wallet to sign it and sends it to your verifier, updating the store at each step (idle→building→signing→verifying→authenticatedorerror). Failures are reported through the store andonError;signIndoes not reject. - Session state:
useSiwxSessionreturnsstatus,session,errorandisAuthenticated;useSiwxSessionStoreis the underlying zustand store for selectors and custom flows. The verified session is saved tolocalStorageand restored after a reload (see Browser Storage below). - Chain agnostic: works with any signer, such as
createEvmSiwxSignerfrom@tuwaio/siwx-evmorcreateSolanaSiwxSignerfrom@tuwaio/siwx-solana, and with any backend, such as the handlers of@tuwaio/siwx-server. - Satellite Connect helpers:
getSatelliteSiwxFields,createSatelliteSiwxSignerandisSessionMatchingConnectionbuild the message fields and the signer from an active Satellite Connect connection. They are duck-typed, so this package does not depend on Satellite Connect.
[!WARNING] The store is UI state only. Server code must read the session from the
HttpOnlycookie issued by your backend (for example withgetSiwxServerSession) and never trust session data sent by the client.
💾 Installation
pnpm add @tuwaio/siwx-react @tuwaio/siwx-core react zustand immer[!IMPORTANT]
@tuwaio/siwx-core,react(>=19.2.3),zustand(>=5) andimmer(>=11) are peer dependencies and must be installed alongside@tuwaio/siwx-react. Add@tuwaio/siwx-evmand/or@tuwaio/siwx-solanafor the wallet signers.
🚀 Usage
Signing in
The example uses the routes of createSiwxApiHandler from @tuwaio/siwx-server/next, mounted at /api/siwx:
import { createEvmSiwxSigner } from '@tuwaio/siwx-evm';
import { useSiwx, useSiwxSession } from '@tuwaio/siwx-react';
import type { WalletClient } from 'viem';
export function SignInButton({ walletClient, address }: { walletClient: WalletClient; address: string }) {
const { signIn, signOut } = useSiwx();
const { status, session, error, isAuthenticated } = useSiwxSession();
const handleSignIn = () =>
signIn({
signer: createEvmSiwxSigner(walletClient),
// The server only accepts nonces it has issued.
getNonce: async () => {
const response = await fetch('/api/siwx/nonce');
const { nonce } = (await response.json()) as { nonce: string };
return nonce;
},
// Returns the session JSON on success, or null.
verifier: async (payload) => {
const response = await fetch('/api/siwx/verify', { method: 'POST', body: JSON.stringify(payload) });
return response.ok ? response.json() : null;
},
fields: {
domain: window.location.host,
uri: window.location.origin,
address: `eip155:1:${address}`,
chainId: 'eip155:1',
statement: 'Sign in to TUWA.',
},
});
const handleSignOut = async () => {
await fetch('/api/siwx/session', { method: 'DELETE' }); // revokes the session and clears the cookie
signOut(); // resets the store
};
if (isAuthenticated) return <button onClick={handleSignOut}>Sign out {session?.address}</button>;
return (
<button onClick={handleSignIn} disabled={status === 'signing' || status === 'verifying'}>
{error ? `Retry sign-in (${error})` : 'Sign in'}
</button>
);
}issuedAtdefaults to now andexpirationTimeto 24 hours later; passfields.expirationTimeto match the server policy (for examplemaxSessionLifetimeSeconds).- Without
getNonceandfields.nonce, the nonce is generated in the browser. Both handlers of@tuwaio/siwx-server/next(durable and demo) reject such nonces, so passgetNoncewith them.
Syncing with the server session
The saved session is restored after a reload until its expirationTime, but the server can end a session earlier (logout in another tab, revoked session, expired cookie). To keep the UI in sync, check the server session when the app starts:
import { type SiwxClientSession, useSiwxSessionStore } from '@tuwaio/siwx-react';
import { useEffect } from 'react';
export function useSyncSiwxSession() {
useEffect(() => {
void fetch('/api/siwx/session')
.then((response) => response.json() as Promise<SiwxClientSession | null>)
.then((session) => {
if (!session) useSiwxSessionStore.getState().reset(); // also clears the saved session
});
}, []);
}Satellite Connect
import {
createSatelliteSiwxSigner,
getSatelliteSiwxFields,
isSessionMatchingConnection,
type MinimalSatelliteConnection,
type SiwxClientSession,
useSiwx,
useSiwxSession,
} from '@tuwaio/siwx-react';
import { useEffect } from 'react';
declare function getNonce(): Promise<string>;
declare function verifier(payload: { message: string; signature: string }): Promise<SiwxClientSession | null>;
export function SatelliteSignIn({ connection }: { connection: MinimalSatelliteConnection }) {
const { signIn, signOut } = useSiwx();
const { session } = useSiwxSession();
// Sign out when the user switches to another account or chain.
useEffect(() => {
if (session && !isSessionMatchingConnection(session, connection)) signOut();
}, [session, connection, signOut]);
const handleSignIn = async () =>
signIn({
signer: await createSatelliteSiwxSigner(connection),
fields: getSatelliteSiwxFields(connection, { statement: 'Sign in to TUWA.', expirationSeconds: 60 * 60 }),
getNonce,
verifier,
});
return <button onClick={handleSignIn}>Sign in</button>;
}getSatelliteSiwxFields treats the connection as EVM when its address starts with 0x or eip155:, its chain ID is a number or starts with eip155:, or it has a connector, and as Solana otherwise. domain and uri default to window.location.host and window.location.href.
🗄️ Browser Storage
The store saves the verified session to localStorage. Clear the key (or call useSiwxSessionStore.getState().reset()) to reset the client state of a user:
| Key | Written by | Content |
|---|---|---|
siwx-react:session | useSiwxSessionStore | { state: { session }, version }: address, chainId, domain, issuedAt and expirationTime of the session, or null |
- Only an
authenticatedsession is saved;resetand a failed or new sign-in writenull. - The saved session is restored when
useSiwxoruseSiwxSessionfirst mounts, after hydration, so server rendering and the first client render are alwaysidle. It is not restored once itsexpirationTimehas passed. Outside React, calluseSiwxSessionStore.persist.rehydrate(). - Nothing is saved until the session has been restored, and nothing is read or written during server rendering or when
localStorageis unavailable. - The session cookie itself is set by your backend as an
HttpOnlycookie that scripts cannot read.
📚 API Reference
Every export, with signatures and types generated from the source, is documented at siwx.docs.tuwa.io/packages/siwx-react .
📄 License
Licensed under the Apache-2.0 License. See the LICENSE file for details.
Interfaces
- MinimalSatelliteConnection
- SatelliteSiwxFieldOptions
- SiwxClientSession
- SiwxMessageFields
- SiwxSessionActions
- SiwxSessionLike
- SiwxSessionState
- UseSiwxReturn
- UseSiwxSignInOptions