ZigZag

Client Setup

Set up the SIWP client plugin for wallet authentication.

Installation

npm i @zig-zag/better-siwp better-auth @talismn/siws

You can also import siwpClient from @zig-zag/better-siwp/client to avoid bundling server-side code (like @talismn/siws and zod) in your client bundle. Both imports export the same siwpClient.

Basic Setup

Add the siwpClient() plugin to your Better Auth client:

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

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

Client Methods

After adding siwpClient(), you get two methods on authClient.siwp:

// Request a nonce (configurable expiry, single use)
const { data, error } = await authClient.siwp.nonce({
  walletAddress: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
});
// data: { nonce: string }

// Verify a signed message and create a session
const { data, error } = await authClient.siwp.verify({
  message: '...',          // The prepared SIWS message string
  signature: '0x...',      // The wallet signature
  walletAddress: '5Grw...', // The signing address
});
// data: { token: string, success: boolean, user: { id: string, walletAddress: string } }

LunoKit handles wallet connection and provides a useSignMessage hook that pairs perfectly with this plugin.

npm i @luno-kit/react @luno-kit/ui @tanstack/react-query @talismn/siws

Sign-in function

lib/auth/polkadot-auth-client.ts
import { SiwsMessage } from '@talismn/siws';
import { authClient } from '@/lib/auth-client';

export async function signInWithPolkadot(
  address: string,
  signMessage: (params: { message: string }) => Promise<{ signature: string }>
) {
  // 1. Get a nonce from the server
  const { data } = await authClient.siwp.nonce({ walletAddress: address });

  // 2. Build a SIWS message
  const siwsMessage = new SiwsMessage({
    domain: window.location.host,
    address,
    statement: 'Sign in with your Polkadot wallet',
    uri: window.location.origin,
    version: '1.0.0',
    nonce: data.nonce,
    issuedAt: Date.now(),
    expirationTime: Date.now() + 24 * 60 * 60 * 1000,
  });
  const message = siwsMessage.prepareMessage();

  // 3. Sign via LunoKit's useSignMessage hook
  const { signature } = await signMessage({ message });

  // 4. Verify with the server — session cookie is set automatically
  await authClient.siwp.verify({ message, signature, walletAddress: address });
}

React component

import { useAccount, useSignMessage } from '@luno-kit/react';
import { useConnectModal } from '@luno-kit/ui';
import { signInWithPolkadot } from '@/lib/auth/polkadot-auth-client';

function AuthButton() {
  const { account } = useAccount();
  const { signMessageAsync } = useSignMessage();
  const { open: openConnectModal } = useConnectModal();

  if (!account) {
    return <button onClick={openConnectModal}>Connect Wallet</button>;
  }

  return (
    <button onClick={() => signInWithPolkadot(account.address, signMessageAsync)}>
      Sign In
    </button>
  );
}

With @polkadot/extension-dapp

If you prefer to work with the Polkadot.js extension APIs directly:

npm i @polkadot/extension-dapp @talismn/siws
import { web3Enable, web3Accounts, web3FromAddress } from '@polkadot/extension-dapp';
import { SiwsMessage } from '@talismn/siws';
import { authClient } from '@/lib/auth-client';

async function signIn() {
  // 1. Connect to wallet extensions
  await web3Enable('My App');
  const accounts = await web3Accounts();
  const account = accounts[0];

  // 2. Get nonce
  const { data } = await authClient.siwp.nonce({ walletAddress: account.address });

  // 3. Build and sign the message
  const siwsMessage = new SiwsMessage({
    domain: window.location.host,
    address: account.address,
    statement: 'Sign in with your Polkadot wallet',
    uri: window.location.origin,
    version: '1.0.0',
    nonce: data.nonce,
    issuedAt: Date.now(),
    expirationTime: Date.now() + 24 * 60 * 60 * 1000,
  });
  const message = siwsMessage.prepareMessage();

  const injector = await web3FromAddress(account.address);
  const { signature } = await injector.signer.signRaw!({
    address: account.address,
    data: message,
    type: 'bytes',
  });

  // 4. Verify — session is created server-side
  await authClient.siwp.verify({ message, signature, walletAddress: account.address });
}

See the examples page for complete working examples.

On this page