BETTER-AUTH. UI
Plugins

Sign In With Ethereum

Install ERC-4361 wallet sign-in and wallet account settings.

The SIWE registry item adds wallet sign-in, optional email collection, and a security settings card for connected wallets.

Setup

Configure Better Auth

Add siwe() to the server. Provide a secure nonce generator and an ERC-4361 verifier. Apply the plugin schema before sign-in.

lib/auth.ts
import { betterAuth } from "better-auth"
import { siwe } from "better-auth/plugins"
import { verifyMessage } from "viem"
import { generateSiweNonce } from "viem/siwe"

export const auth = betterAuth({
  plugins: [
    siwe({
      domain: "app.example.com",
      getNonce: async () => generateSiweNonce(),
      verifyMessage: async ({ message, signature, address }) =>
        verifyMessage({
          address: address as `0x${string}`,
          message,
          signature: signature as `0x${string}`
        })
    })
  ]
})

See the Better Auth SIWE guide for the complete server setup.

Add the client plugin

lib/auth-client.ts
import { createAuthClient } from "better-auth/react"
import { siweClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({ plugins: [siweClient()] })

Install the registry item

npx shadcn@latest add @better-auth-ui/siwe

Register the UI plugin

components/providers.tsx
import { createEip1193WalletConnector } from "@better-auth-ui/core/plugins/siwe"
import { siwePlugin } from "@/lib/auth/siwe-plugin"

<AuthProvider
  authClient={authClient}
  navigate={navigate}
  plugins={[
    siwePlugin({
      connector: createEip1193WalletConnector(),
      domain: "app.example.com",
      uri: "https://app.example.com",
      email: "optional"
    })
  ]}
>
  {children}
</AuthProvider>

Wallet settings

Better Auth does not expose browser endpoints for SIWE wallet management. Create authenticated server routes and connect them through SiweWalletManager.

Authorize on the server

Resolve the user from the server session. Do not accept a user ID from the browser for wallet operations.

lib/wallet-manager.ts
import type {
  SiweWalletAccount,
  SiweWalletLinkChallenge,
  SiweWalletManager
} from "@better-auth-ui/core/plugins/siwe"

const assertOk = async (response: Response) => {
  if (!response.ok) {
    throw new Error(`Wallet request failed with status ${response.status}.`)
  }
}

const get = async <TResponse>(
  url: string,
  signal?: AbortSignal
): Promise<TResponse> => {
  const response = await fetch(url, { signal })
  await assertOk(response)
  return response.json() as Promise<TResponse>
}

const post = async <TResponse = void>(
  url: string,
  body: unknown
): Promise<TResponse> => {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  })
  await assertOk(response)
  if (response.status === 204) return undefined as TResponse
  return response.json() as Promise<TResponse>
}

export const walletManager: SiweWalletManager = {
  list: (signal) => get<SiweWalletAccount[]>("/api/wallets", signal),
  createLinkChallenge: (wallet) =>
    post<SiweWalletLinkChallenge>("/api/wallets/link-challenge", wallet),
  link: (proof) => post("/api/wallets/link", proof),
  unlink: (walletId) => fetch(`/api/wallets/${walletId}`, { method: "DELETE" }).then(assertOk),
  setPrimary: (walletId) => fetch(`/api/wallets/${walletId}/primary`, { method: "POST" }).then(assertOk)
}

Pass walletManager to siwePlugin(). The registry item then adds connect, list, primary, and remove controls.

The link challenge must use a single-use nonce. Verify its domain, chain, address, nonce, and signature before attaching the wallet.

Use email: "required" to require an email. Use email: "none" to open the wallet without an email dialog.

The wallet signature does not verify the supplied email address.

Last updated on

On this page