ZigZag

Server Setup

Configure the SIWP plugin on your server with Better Auth.

Prerequisites

  • A Better Auth instance (setup guide)
  • A database adapter configured with Better Auth (Drizzle, Prisma, etc.)
  • better-auth v1.5.0 or higher

Installation

npm i @zig-zag/better-siwp better-auth zod

Basic Setup

Add the siwp() plugin to your Better Auth config:

lib/auth.ts
import { betterAuth } from 'better-auth';
import { siwp } from '@zig-zag/better-siwp';

export const auth = betterAuth({
  database: yourAdapter,
  plugins: [
    siwp({
      domain: 'example.com',
    }),
  ],
});

Configuration Options

The siwp() plugin accepts an optional config object:

siwp({
  // Optional: your domain without protocol
  // If not set, auto-detected from the request Origin header
  domain: 'example.com',

  // Optional: nonce lifetime in seconds (default: 900 = 15 minutes)
  nonceExpiresIn: 600, // 10 minutes

  // Optional: custom nonce generation
  getNonce: async () => {
    return crypto.randomUUID();
  },

  // Optional: custom signature verification
  verifyMessage: async ({ message, signature, address }) => {
    // Your custom logic — return true if valid
    return true;
  },

  // Optional: extract user info from the wallet
  getUserInfo: async ({ message, address, signature }) => {
    return {
      name: 'Alice',
      email: 'alice@example.com',
      image: 'https://example.com/avatar.png',
    };
  },

  // Optional: domain for generated email addresses
  // Default: uses the resolved domain (e.g., "5Grw...@example.com")
  emailDomainName: 'users.example.com',
})

Options

OptionTypeRequiredDefaultDescription
domainstringNoAuto-detected from Origin or Host headerDomain for the SIWS message
nonceExpiresInnumberNo900 (15 minutes, in seconds)Nonce lifetime in seconds
getNonce() => Promise<string>NoRandom alphanumeric stringCustom nonce generation
verifyMessage(params) => Promise<boolean>NoverifySIWS from @talismn/siwsCustom signature verification
getUserInfo(params) => Promise<UserInfo>NoName from truncated addressExtract user info from the wallet
emailDomainNamestringNoSame as resolved domainDomain for generated email addresses

When your API and frontend run on different ports (e.g., API on 3001, frontend on 3000), the domain is auto-detected from the Origin header sent by the browser. If you need explicit control, set the domain option.

Server Exports

import { siwp, parseMessage, verifySIWS } from '@zig-zag/better-siwp';
import type { SIWPOptions, SiwsMessage } from '@zig-zag/better-siwp';
ExportDescription
siwp(options)Better Auth server plugin
parseMessage(message)Parse a SIWS message string (re-exported from @talismn/siws)
verifySIWS(message, signature, address)Verify a SIWS signature (re-exported from @talismn/siws)
SIWPOptionsPlugin configuration type
SiwsMessageSIWS message type

API Routes

The plugin registers two endpoints on your Better Auth instance. Both use POST:

EndpointMethodBodyResponse
/api/auth/siwp/noncePOST{ walletAddress: string }{ nonce: string }
/api/auth/siwp/verifyPOST{ message: string, signature: string, walletAddress: string }{ token: string, success: boolean, user: { id: string, walletAddress: string } }

Next.js App Router

app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';

export const { GET, POST } = toNextJsHandler(auth);

On this page