Skip to Content
Introduction

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.000Z

Architecture

SIWX is designed with strict separation of concerns within the TUWA layer model:

LayerPackageResponsibility
L1@tuwaio/siwx-coreLow-level CAIP-122 Engine (zero dependencies, completely standalone)
L2@tuwaio/siwx-evm, solana, react, serverMulti-chain Adapters, React bindings & Server utilities
L3satelliteWallet Connection & Session Integration Layer
L7nova-uikitUI 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 serialization

Authentication 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/session or POST /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-server works with Next.js, NestJS, Hono, Express, and Cloudflare Workers.
  • No State in SDK: siwx-react manages 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.

Last updated on