BETTER-AUTH. UI
Plugins

Sign In With Ethereum

Add ERC-4361 wallet sign-in, optional email collection, and wallet account settings.

The SIWE plugin adds an Ethereum wallet button to the sign-in form. It requests a nonce, builds an ERC-4361 message, signs it, and verifies it with Better Auth.

You can also add a wallet settings card. The card lists connected wallets, changes the primary wallet, and removes wallets.

Setup

Configure Better Auth

Add siwe() to the server. Supply a secure nonce generator and an ERC-4361 message verifier.

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}`
        })
    })
  ]
})

Apply the SIWE schema before using the plugin. See the Better Auth SIWE guide.

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()] })

Register the UI plugin

The included connector uses an injected EIP-1193 wallet. You can provide another connector for Wagmi or another wallet library.

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

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

Wallet settings

Better Auth does not expose browser endpoints for listing or changing SIWE wallets. Add authenticated server routes, then connect them with SiweWalletManager.

Authorize on the server

Never trust a user ID from the browser. Resolve the user from the server session for every wallet operation.

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(). BAUI then adds connect, list, primary, and remove controls to security settings.

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

The server must delete a wallet's walletAddress row and matching SIWE account in one transaction.

Email modes

  • "optional" asks for an email but allows an empty value. This is the default.
  • "required" requires an email before the wallet opens.
  • "none" opens the wallet immediately.

The signature proves control of the wallet. It does not verify the supplied email address.

Last updated on

On this page