Sign-In With X (SIWX)
@tuwaio/siwx provides the foundational Low-Level Core & Adapters Layer (L1/L2) of the TUWA Ecosystem. It implements the CAIP-122 (Sign-In With X) standard — a chain-agnostic, interoperable format for authenticating blockchain accounts across any network.
Multi-Chain CAIP-122 Authentication
@tuwaio/siwx establishes a unified authentication protocol across heterogenous blockchain networks. By leveraging CAIP-2 chain identifiers (e.g., eip155:1, solana:5eykt4...) and CAIP-10 account representations, it enables seamless, spec-compliant sign-in flows regardless of the underlying network or frontend framework.
A CAIP-122 message looks like:
app.tuwa.io wants you to sign in with your blockchain account:
eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
Sign in to TUWA.
URI: https://app.tuwa.io
Version: 1
Chain ID: eip155:1
Nonce: a4f3b2c1d0e5f6789abc
Issued At: 2026-08-06T08:00:00.000Z
Expiration Time: 2026-08-06T08:10:00.000ZArchitecture
SIWX is designed with strict separation of concerns within the TUWA layer model:
| Layer | Package | Responsibility |
|---|---|---|
| L1 | @tuwaio/siwx-core | Low-level CAIP-122 Engine (zero dependencies, completely standalone) |
| L2 | @tuwaio/siwx-evm, solana, react, server | Multi-chain Adapters, React bindings & Server utilities |
| L3 | satellite | Wallet Connection & Session Integration Layer |
| L7 | nova-uikit | UI View Layer (consumes siwx-react) |
Package Structure
@tuwaio/siwx-core → Message building, parsing, validation (zero deps)
@tuwaio/siwx-evm → EIP-191 + EIP-1271 verification via viem
@tuwaio/siwx-solana → ed25519 verification via SubtleCrypto + @solana/kit
@tuwaio/siwx-react → Zustand session store + React hooks
@tuwaio/siwx-server → Backend verification dispatcher + cookie serializationAuthentication Flow
Client Wallet Your Backend
│ │ │
│── GET /api/siwx/nonce ──────────────────────────> │ (issues challenge nonce)
│<── { nonce } ─────────────────────────────────── │
│── buildMessage(fields) ──> │ │
│── signMessage(message) ──> │ │
│<── signature ─────────────── │ │
│── POST /api/siwx/verify ──────────────────────────> │
│ │── verifySiwxPayload()
│ │── nonceStore.consume()
│ │── sessionStore.create()
│<── Set-Cookie: siwx-session-v2 ───────────────── │
│── useSiwxSession() shows authenticated │Integrations
SIWX is completely headless, giving you full control over how you wire up the frontend and backend.
1. Backend (Next.js Example)
Option A: Production Standard (Durable Redis Session & Nonce Store)
// lib/authStores.ts
import type { SiwxNonceStore, SiwxSession, SiwxSessionRecord, SiwxSessionStore } from '@tuwaio/siwx-server';
import { generateServerNonce } from '@tuwaio/siwx-server';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
export const sessionStore: SiwxSessionStore = {
async create({ session, ttlSeconds }: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord> {
const id = generateServerNonce();
const createdAt = Date.now();
const expiresAt = createdAt + ttlSeconds * 1000;
const record: SiwxSessionRecord = { id, session, createdAt, expiresAt };
await redis.set(`siwx:session:${id}`, JSON.stringify(record), 'EX', ttlSeconds);
return record;
},
async get(id: string): Promise<SiwxSessionRecord | null> {
const data = await redis.get(`siwx:session:${id}`);
return data ? JSON.parse(data) : null;
},
async bindSubject(id: string, subjectId: string): Promise<boolean> {
const record = await this.get(id);
if (!record) return false;
record.subjectId = subjectId;
const ttl = Math.max(1, Math.floor((record.expiresAt - Date.now()) / 1000));
await redis.set(`siwx:session:${id}`, JSON.stringify(record), 'EX', ttl);
return true;
},
async revoke(id: string): Promise<void> {
await redis.del(`siwx:session:${id}`);
},
};
export const nonceStore: SiwxNonceStore = {
async issue({ nonce, ttlSeconds }: { nonce: string; ttlSeconds: number }): Promise<void> {
await redis.set(`siwx:nonce:${nonce}`, '1', 'EX', ttlSeconds);
},
async consume({ nonce }: { nonce: string }): Promise<boolean> {
const value = await redis.getdel(`siwx:nonce:${nonce}`);
return value !== null;
},
};In-Memory Alternative for Local Testing / Prototyping (No Redis Needed):
// lib/authStores.dev.ts (Zero Dependencies / In-Memory)
import { MemorySiwxNonceStore, MemorySiwxSessionStore } from '@tuwaio/siwx-server';
// Built-in in-memory stores for local testing (fails closed in production by default)
export const sessionStore = new MemorySiwxSessionStore();
export const nonceStore = new MemorySiwxNonceStore();Route handler setup:
// app/api/siwx/[...siwx]/route.ts
import { createSiwxApiHandler } from '@tuwaio/siwx-server/next';
import { sessionStore, nonceStore } from '@/lib/authStores';
const handler = createSiwxApiHandler({
sessionStore,
nonceStore,
policy: { expectedDomain: 'app.tuwa.io' },
});
export const { GET, POST, DELETE } = handler;Option B: Stateless Demo Profile (Zero-Infrastructure Demonstration & Prototyping)
// app/api/siwx/[...siwx]/route.ts
import { createStatelessDemoSiwxHandler } from '@tuwaio/siwx-server/next';
const handler = createStatelessDemoSiwxHandler({
signingSecret: process.env.SIWX_DEMO_SIGNING_SECRET!, // Minimum 32 characters
policy: { expectedDomain: 'demo.tuwa.io', requireExpirationTime: true },
});
export const { GET, POST, DELETE } = handler;Option C: Server Actions Session Verification (getSiwxServerSession)
// app/actions/myAction.ts
'use server';
import { cookies } from 'next/headers';
import { getSiwxServerSession } from '@tuwaio/siwx-server';
import { isSessionMatchingTarget } from '@tuwaio/siwx-core';
import { sessionStore } from '@/lib/authStores';
export async function myAction(targetAddress: string) {
const session = await getSiwxServerSession({
cookieSource: await cookies(),
sessionStore, // Or signingSecret for demo profile
});
if (!session || !isSessionMatchingTarget(session, targetAddress)) {
throw new Error('Unauthorized');
}
return { success: true };
}This creates standard endpoints natively compatible with the React state store:
GET /api/siwx/session(Retrieves the active session)GET/POST /api/siwx/nonce(Issues challenge nonces)POST /api/siwx/verify(Verifies signature, consumes nonce atomically, and issues session cookie)DELETE /api/siwx/sessionorPOST /api/siwx/logout(Revokes session in store and clears cookie)
2. Frontend (React Example)
Use the useSiwx hook to trigger the authentication flow. You must provide a signer function tailored to the connected chain, and a verifier function that calls your backend.
For convenience, @tuwaio/siwx-evm and @tuwaio/siwx-solana provide standard signer adapters (createEvmSiwxSigner and createSolanaSiwxSigner).
import { useSiwx } from '@tuwaio/siwx-react';
import { createEvmSiwxSigner } from '@tuwaio/siwx-evm';
// import { createSolanaSiwxSigner } from '@tuwaio/siwx-solana';
const { signIn, signOut } = useSiwx();
const handleSignIn = async () => {
// 1. Resolve your signer adapter based on the active wallet connection
// Examples: createEvmSiwxSigner(walletClient) or createSolanaSiwxSigner(connectedAccount)
const signer = createEvmSiwxSigner(walletClient);
// 2. Trigger the SIWX flow
await signIn({
signer,
verifier: async (payload) => {
// POST to the `verify` action from createSiwxApiHandler
const res = await fetch('/api/siwx/verify', {
method: 'POST',
body: JSON.stringify(payload),
});
return res.ok ? res.json() : null;
},
fields: {
domain: window.location.host,
uri: window.location.origin,
address: `eip155:1:${address}`, // Strict CAIP-10 format
chainId: 'eip155:1', // Strict CAIP-2 format
statement: 'Sign in to TUWA.',
},
});
};Design Principles
- Headless First: Zero UI. This library is pure logic.
- Backend-Agnostic:
siwx-serverworks with Next.js, NestJS, Hono, Express, and Cloudflare Workers. - No State in SDK:
siwx-reactmanages client state independently using Zustand. - Strict Standard: Every message is CAIP-122 spec-compliant. Parser and builder are round-trip compatible.
📚 Technical Reference
Refer to the API Reference to explore generated TypeDoc definitions for all exported types, functions, and interfaces.