Authentication
The Web3 variant replaces the base package's email/OAuth-first authentication with SIWE (Sign-In With Ethereum) — users authenticate by signing a message with their wallet. Email and OAuth authentication remain available as secondary options.
How SIWE Works
SIWE is a standard for using Ethereum wallets to authenticate with web applications. Instead of passwords or OAuth tokens, the user proves ownership of their wallet address by signing a structured message.
The authentication flow:
- User connects wallet — RainbowKit prompts the user to select and connect a wallet
- Client requests SIWE message — The
generateSiweMessageGraphQL mutation creates a message containing the domain, address, chain ID, and a random nonce - Server generates message — The server creates a SIWE-formatted message with the nonce stored for verification
- User signs message — The wallet prompts the user to sign the message (no gas cost)
- Client sends signature — The signed message is sent to
authenticateWithSiwe - Server verifies — The server verifies the signature matches the address, checks the domain and nonce, then creates or retrieves the user account
- JWT issued — A JWT token is returned containing
userIdandweb3_walletfields
# Step 1: Generate SIWE message
mutation {
generateSiweMessage(
address: "0x1234..."
chainId: 1
domain: "localhost:3000"
) {
message
nonce
}
}
# Step 2: Authenticate with signed message
mutation {
authenticateWithSiwe(
message: "localhost:3000 wants you to sign in..."
signature: "0xabcd..."
) {
success
token
web3Wallet
error
}
}
Domain Validation
The server validates that the SIWE message domain matches an allowed origin. Configure allowed origins with WEB3_ALLOWED_SIWE_ORIGINS:
# .env.local
WEB3_ALLOWED_SIWE_ORIGINS=http://localhost:3000,https://myapp.com
In production, this prevents phishing attacks where a malicious site could trick users into signing messages for your domain.
Frontend AuthContext
The Web3 variant's AuthContext is a state machine built on useReducer that coordinates wallet connection with SIWE authentication.
State Machine
IDLE ──────────────────► AUTHENTICATING ──► AUTHENTICATED
│ │ │
│ ├──► REJECTED │
│ │ │
│ └──► ERROR │
│ │
└── (on mount) ──► RESTORING ──► WAITING_FOR_WALLET ──► AUTHENTICATED
│
└──► IDLE (no saved session)
Key transitions:
- On mount, the context checks localStorage for a saved token. If found, it validates with the server and enters
WAITING_FOR_WALLETuntil the wallet reconnects - When a wallet connects and the user is in
IDLE, authentication starts automatically (unless the user previously rejected signing for that address) - If the user rejects the signature prompt, the state moves to
REJECTEDand auto-authentication is suppressed for that address until a different wallet connects - Wallet disconnection while authenticated triggers an automatic logout
Interface
interface AuthContextValue {
isAuthenticated: boolean
isLoading: boolean
error: Error | null
authToken: string | null
walletAddress: string | null
userRejectedAuth: boolean
authenticate: (address: string) => Promise<AuthResult>
logout: () => void
restoreAuth: () => void
}
The walletAddress and userRejectedAuth properties are specific to the Web3 variant. The authenticate method takes a wallet address and runs the full SIWE flow.
Auto-Authentication
When a wallet connects and the user hasn't rejected signing for that address, the context automatically calls authenticate(address) after a short delay to ensure the wallet connector is fully ready. This provides a seamless experience — connecting a wallet immediately triggers sign-in.
If the user rejects the signature, the address is tracked so auto-authentication won't trigger again until a different wallet is connected or the page is refreshed.
JWT Token
The JWT token includes a web3_wallet field alongside the standard userId:
{
"type": "auth",
"userId": 42,
"web3_wallet": "0x1234...abcd",
"iat": 1700000000,
"exp": 1700086400,
"jti": "unique-token-id"
}
The validateToken query also returns the web3Wallet field, which the frontend uses during session restoration to verify the saved wallet matches.
ConnectWallet Component
The ConnectWallet component uses RainbowKit's ConnectButton.Custom API to render wallet connection UI. It shows different states based on the connection and authentication status:
- Disconnected — A "Connect Wallet" button
- Wrong network — A warning with a chain switcher
- Connected — The wallet address and account details
- Optional
showNetworkprop — Adds a chain selector button alongside the account
The component coordinates with AuthContext — connecting a wallet triggers SIWE authentication automatically.
Account Linking
Users who authenticate with SIWE can also link email and OAuth accounts. The web3 wallet address is stored as an auth method in the userAuth table alongside any other linked methods. This means a user who initially signed in with their wallet can later add email authentication to the same account.
When authenticating with email or OAuth, the server checks if the user already has a web3 wallet linked and includes it in the JWT token, maintaining wallet context across auth methods.