ZigZag

Overview

Sign In With Polkadot — a Better Auth plugin for wallet authentication.

@zig-zag/better-siwp

Sign In With Polkadot (SIWP) plugin for Better Auth. Add wallet-based authentication to any Polkadot application with a single plugin. Users connect their wallet, sign a message, and get a server-side session.

Built on the SIWS standard by Talisman. Works with LunoKit, Dedot, @polkadot/extension-dapp, or any compatible Polkadot wallet extension.

Install

npm i @zig-zag/better-siwp

Peer dependencies:

npm i better-auth zod

Requires better-auth v1.5.0 or higher. The plugin uses internal APIs that are not available in earlier versions. If you see errors like deleteVerificationValue is not a function, check your better-auth version.

Quick Start

Server

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

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

Client

import { createAuthClient } from 'better-auth/client';
import { siwpClient } from '@zig-zag/better-siwp/client';

export const authClient = createAuthClient({
  plugins: [siwpClient()],
});

That's it. Your app now has nonce generation, signature verification, session management, and cookie-based auth — all handled.

How It Works

1. User clicks "Connect Wallet"
   └─ Wallet extension shows available accounts

2. User selects an account
   └─ App calls authClient.siwp.nonce({ walletAddress })
   └─ Server generates a nonce, stores it (15 min expiry)

3. App builds a SIWS message with the nonce
   └─ Using SiwsMessage from @talismn/siws

4. User signs the message in their wallet
   └─ sr25519 signature, private key never leaves the extension

5. App calls authClient.siwp.verify({ message, signature, walletAddress })
   └─ Server validates: address match, domain match, nonce match
   └─ Server verifies the cryptographic signature
   └─ Server creates or finds the user
   └─ Server creates a session and sets a cookie

6. User is authenticated
   └─ Session persists across page loads via Better Auth

The nonce is deleted after verification — each signature is single-use (replay protection).

Features

  • 3-line setup — Plugin architecture, minimal config
  • Any wallet — Talisman, SubWallet, Polkadot.js, and more
  • Any chain — Works with any Substrate chain
  • Session management — Cookie-based auth with Better Auth
  • Type-safe — Full TypeScript support
  • Error handling — Structured error codes for every failure case

Error Handling

Verification errors return structured responses with machine-readable error codes:

const { data, error } = await authClient.siwp.verify({ message, signature, walletAddress });

if (error) {
  switch (error.code) {
    case 'ADDRESS_MISMATCH':
      // Address in message doesn't match walletAddress
      break;
    case 'DOMAIN_MISMATCH':
      // Domain in message doesn't match server domain
      break;
    case 'INVALID_NONCE':
      // Nonce expired or doesn't match
      break;
    case 'INVALID_SIGNATURE':
      // Wallet signature verification failed
      break;
  }
}
Error CodeHTTP StatusWhen
ADDRESS_MISMATCH400Wallet address in SIWS message doesn't match the walletAddress parameter
DOMAIN_MISMATCH400Domain in SIWS message doesn't match the server's domain
INVALID_NONCE401Nonce expired (default: 15 min) or was already used
INVALID_SIGNATURE401Cryptographic signature verification failed

On this page