Skip to content

Instantly share code, notes, and snippets.

@b-bot
Created July 1, 2025 10:28
Show Gist options
  • Save b-bot/a824c2e5e49f43ad4de448a94424a7d2 to your computer and use it in GitHub Desktop.
Save b-bot/a824c2e5e49f43ad4de448a94424a7d2 to your computer and use it in GitHub Desktop.
Better Auth SIWE Docs
{
title: "SIWE",
href: "/docs/plugins/siwe",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m11.944 17.97-7.364-4.35 7.363 10.38 7.37-10.38-7.372 4.35zm.112-17.97-7.366 12.223 7.365 4.354 7.365-4.35z"
/>
</svg>
),
},
title description
SIWE
Sign in with Ethereum plugin for Better Auth

The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the ERC-4361 standard. This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic.

Installation

### Add the Server Plugin
    Add the SIWE plugin to your auth configuration:

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import { siwe } from "better-auth/plugins";

    export const auth = betterAuth({
        plugins: [
            siwe({
                domain: "example.com",
                emailDomainName: "example.com", // optional
                anonymous: false, // optional, default is true
                getNonce: async () => {
                    // Implement your nonce generation logic here
                    return "your-secure-random-nonce";
                },
                verifyMessage: async (args) => {
                    // Implement your SIWE message verification logic here
                    // This should verify the signature against the message
                    return true; // return true if signature is valid
                },
                ensLookup: async (args) => {
                    // Optional: Implement ENS lookup for user names and avatars
                    return {
                        name: "user.eth",
                        avatar: "https://example.com/avatar.png"
                    };
                },
            }),
        ],
    });
    ```
</Step>

<Step>
    ### Migrate the database

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items={["migrate", "generate"]}>
        <Tab value="migrate">
        ```bash
        npx @better-auth/cli migrate
        ```
        </Tab>
        <Tab value="generate">
        ```bash
        npx @better-auth/cli generate
        ```
        </Tab>
    </Tabs>
    See the [Schema](#schema) section to add the fields manually.
</Step>

<Step>
    ### Add the Client Plugin

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client";
    import { siweClientPlugin } from "better-auth/client/plugins";

    export const authClient = createAuthClient({
        plugins: [siweClientPlugin()],
    });
    ```
</Step>

Usage

Generate a Nonce

Before signing a SIWE message, you need to generate a nonce for the wallet address:

const { data, error } = await authClient.siwe.nonce({
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  chainId: 1, // optional, defaults to 1 (Ethereum mainnet)
});

if (data) {
  console.log("Nonce:", data.nonce);
}

Sign In with Ethereum

After generating a nonce and creating a SIWE message, verify the signature to authenticate:

const { data, error } = await authClient.siwe.verify({
  message: "Your SIWE message string",
  signature: "0x...", // The signature from the user's wallet
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  chainId: 1, // optional, defaults to 1
  email: "[email protected]", // optional, required if anonymous is false
});

if (data) {
  console.log("Authentication successful:", data.user);
}

Configuration Options

Server Options

The SIWE plugin accepts the following configuration options:

  • domain: The domain name of your application (required for SIWE message generation)
  • emailDomainName: The email domain name for creating user accounts when not using anonymous mode. Defaults to the domain from your base URL
  • anonymous: Whether to allow anonymous sign-ins without requiring an email. Default is true
  • getNonce: Function to generate a unique nonce for each sign-in attempt. You must implement this function to return a cryptographically secure random string. Must return a Promise<string>
  • verifyMessage: Function to verify the signed SIWE message. Receives message details and should return Promise<boolean>
  • ensLookup: Optional function to lookup ENS names and avatars for Ethereum addresses

Client Options

The SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility:

import { createAuthClient } from "better-auth/client";
import { siweClientPlugin } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    siweClientPlugin({
      // Optional client configuration can go here
    }),
  ],
});

Schema

The SIWE plugin adds a walletAddress table to store user wallet associations:

Field Type Description
id string Primary key
userId string Reference to user.id
address string Ethereum wallet address
chainId number Chain ID (e.g., 1 for Ethereum mainnet)
isPrimary boolean Whether this is the user's primary wallet
createdAt date Creation timestamp

Example Implementation

Here's a complete example showing how to implement SIWE authentication:

import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

export const auth = betterAuth({
  database: {
    // your database configuration
  },
  plugins: [
    siwe({
      domain: "myapp.com",
      emailDomainName: "myapp.com",
      anonymous: false,
      getNonce: async () => {
        // Generate a cryptographically secure random nonce
        return generateRandomString(32);
      },
      verifyMessage: async ({ message, signature, address }) => {
        try {
          // Verify the signature using viem
          const isValid = await verifyMessage({
            address: address as `0x${string}`,
            message,
            signature: signature as `0x${string}`,
          });
          return isValid;
        } catch (error) {
          console.error("SIWE verification failed:", error);
          return false;
        }
      },
      ensLookup: async ({ walletAddress }) => {
        try {
          // Optional: lookup ENS name and avatar using viem
          // You can use viem's ENS utilities here
          const client = createPublicClient({
            chain: mainnet,
            transport: http(),
          });

          const ensName = await client.getEnsName({
            address: walletAddress as `0x${string}`,
          });

          const ensAvatar = ensName
            ? await client.getEnsAvatar({
                name: ensName,
              })
            : null;

          return {
            name: ensName || walletAddress,
            avatar: ensAvatar || "",
          };
        } catch {
          return {
            name: walletAddress,
            avatar: "",
          };
        }
      },
    }),
  ],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment