Wallets, chains and endpoints
The two chain lists that must agree, which wallets can actually complete a sign-in, every endpoint the addon adds, and every variable it reads.
Everything on this page is fixed in code. There is no admin screen and no environment variable that changes any of it.
Chains
There are two lists and they are maintained separately.
The frontend picker — frontend/config/wallet.tsx — decides which networks
the Reown modal offers and what Chain ID: ends up in the signed message.
The backend allow-list — backend/src/api/auth/utils.ts — decides which
chain IDs are accepted at verification. A chain outside it is refused with 401
and an Unsupported SIWE chainId line in the backend log, before any network
call is made.
| Chain | eip155 ID |
Offered by the picker | Accepted by the backend |
|---|---|---|---|
| Ethereum Mainnet | 1 | Yes — the default network | Yes |
| Optimism | 10 | Yes | Yes |
| BNB Smart Chain | 56 | Yes | Yes |
| Polygon | 137 | Yes | Yes |
| Arbitrum One | 42161 | Yes | Yes |
| Base | 8453 | Yes | Yes |
| Avalanche C-Chain | 43114 | No | Yes |
| Sepolia (testnet) | 11155111 | No | Yes |
Two behaviours follow.
Avalanche and Sepolia are unreachable in stock form. The backend would accept them; the picker never produces them. They only become usable if you add them to the frontend list.
A message with no Chain ID: line is treated as Ethereum mainnet. The
parser defaults to 1 when the line is absent, so verification is routed to
mainnet rather than refused.
The chain ID from the user-supplied message is interpolated into the RPC URL that verifies the signature. Widening the list to accept an arbitrary value would let a caller point verification at an endpoint of their choosing, which is the same as letting them decide whether their own signature is valid.
If you add a chain, add a specific eip155:<id> entry to the backend set. Never
remove the check.
Adding a chain means editing both files and then rebuilding the frontend and restarting the backend. Adding it to only one produces a network users can pick and cannot sign in on.
Wallets
The Reown modal lists every wallet in the WalletConnect registry, and any of them can complete the connect step. Completing the sign step is narrower.
Both the login form and the profile Wallet tab build an ethers provider from
window.ethereum to request the signature, rather than from the AppKit
connector that the user just chose. If no injected provider exists, the flow
stops with "Ethereum provider not found. Please install MetaMask."
In practice:
| Situation | Connect | Sign |
|---|---|---|
| Desktop browser with MetaMask, Rabby, Coinbase Wallet extension or similar | Works | Works |
| Mobile wallet's in-app browser (MetaMask, Trust, Coinbase) | Works | Works — those browsers inject a provider |
| Desktop browser, no extension, WalletConnect QR to a phone | Works | Fails — no injected provider to sign with |
| Smart-contract wallet (Safe and similar) reached through an injected provider | Works | Works — verification falls through to EIP-1271 on the named chain |
There is a second consequence of reading the injected provider directly: the address that signs is whichever account that provider currently has selected, which is not necessarily the address AppKit is displaying. A user with several accounts in one extension can connect as one and sign as another, and the sign-in then fails with "Wallet address not recognized" for an address they never intended to use.
The four names shown on the profile Wallet tab — MetaMask, WalletConnect, Coinbase, Trust Wallet — are static labels in that page, not a capability list.
Endpoints
All three are added by this addon. None of them carries a permission key; two are public and one requires a session.
Request and response shapes
POST /api/auth/login/wallet and POST /api/user/profile/wallet/connect take
the same body:
{
"message": "<the full SIWE message the user signed>",
"signature": "0x…"
}A successful sign-in returns a message plus the three cookies the framework
converts into Set-Cookie headers — accessToken, sessionId and csrfToken.
A sign-in that hits the 2FA gate returns 200 with twoFactor,
twoFactorToken and no cookies at all; see
The sign-in flow.
POST /api/user/profile/wallet/disconnect takes { "address": "0x…" }.
Where the linked address is readable
The providers array is the only place a linked wallet is exposed by the API.
Each entry is { provider, providerUserId }. There is no admin endpoint that
lists wallet links.
Licence and enable gating
| Thing | Value |
|---|---|
Extension name (the extension table) |
wallet_connect |
| Envato item ID / product ID | 37548018 |
| Licence file the handlers check for | lic/37548018.lic in the project root |
| Store slug | wallet-connect |
| Admin permissions to enable it | view.extension, edit.extension |
Unlike every other addon, Wallet Connect has no route prefix registered for
licence enforcement. Its endpoints sit under /api/auth and
/api/user/profile, both of which are on the licence-exempt list so that login
survives a core licence being re-activated. The check therefore runs inside the
three gated handlers, and it tests two things: that wallet_connect is present
in the enabled-extension cache, and that the .lic file exists on disk. Either
one failing returns 403 "Wallet authentication is not enabled on this
platform" or 403 "Wallet Connect extension license is not activated".
The enabled-extension cache only ever holds rows with status = true, and is
invalidated when an admin toggles an extension, so the toggle takes effect
without a restart.
Environment variables
The addon reads no others. It has no rows in the settings table, and the 2FA
behaviour it inherits is governed by the core's own twoFactorStatus,
twoFactorEmailStatus and twoFactorSmsStatus settings.
Outbound network requirements
| From | To | Purpose | If blocked |
|---|---|---|---|
| Browser | Reown relay and registry | Wallet discovery and the WalletConnect session | The picker fails to load wallets |
| Backend | rpc.walletconnect.org |
Signature verification | Every sign-in returns 401 |
The backend leg is the one people miss. A server with locked-down egress will verify nothing, and the failure looks exactly like a bad signature: the wallet prompt succeeds, the user is refused, and there is a Signature verification error line in the backend log.
Data model
-- provider_user (shared with Google sign-in)
id CHAR(36) PRIMARY KEY
userId CHAR(36) NOT NULL -- FK user.id, ON DELETE CASCADE
providerUserId VARCHAR(255) NOT NULL -- UNIQUE across both provider types, LOWERCASED on write
provider ENUM('GOOGLE','WALLET') NOT NULL
isPrimary TINYINT(1) -- TRUE or NULL, never FALSE
chainId INT -- EIP-155 chain the signature was proven on
verifiedAt DATETIME -- when that signature was verified
createdAt / updatedAt / deletedAt -- soft-delete columns
UNIQUE KEY (providerUserId)
UNIQUE KEY (userId, provider, isPrimary)The user table also carries walletAddress and walletProvider. They are a
denormalised copy of the primary WALLET link, written only by the
afterSave and afterDestroy hooks on providerUser and defended by a guard on
the user model that throws for every other writer. They are NULL for a user
with no linked wallet, and they never hold a second linked address — read
Linking a wallet before you rely on either.
Package versions
Declared across the root and frontend package manifests. These are the
versions this addon's behaviour is described against.
| Package | Version |
|---|---|
@reown/appkit |
1.8.15 |
@reown/appkit-adapter-wagmi |
1.8.15 |
wagmi |
^2.19.5 |
viem |
^2.55.10 |
ethers |
^6.17.0 |
siwe |
^3.0.0 |