@siws/better-auth

Drop-in Sign In With Solana plugin for better-auth with server and client support, automatic nonce management, and session handling.

A better-auth plugin that handles the complete SIWS flow -- nonce generation, message building, signature verification, user management, and session creation.

Server Plugin

auth.ts
auth.ts
import { betterAuth } from 'better-auth'
import { siws } from '@siws/better-auth'
 
export const auth = betterAuth({
  // ... your database and other config
  plugins: [
    siws({
      statement: 'Sign in to MyApp',
      expirationTime: 600, // 10 minutes
      cluster: 'mainnet-beta',
    }),
  ],
})

Options

OptionTypeDefaultDescription
statementstring?--Statement shown when signing
expirationTimenumber?300Expiration in seconds
clusterCluster?'mainnet-beta'Default Solana cluster
resourcesstring[]?--Resource URIs to include
emailDomainNamestring?'siws.local'Domain for synthetic emails
anonymousboolean?trueAllow sign-up without email
schemaobject?--Custom solanaAddress table schema

Client Plugin

auth-client.ts
auth-client.ts
import { createAuthClient } from 'better-auth/client'
import { siwsClient } from '@siws/better-auth/client'
 
export const auth = createAuthClient({
  plugins: [siwsClient()],
})

Client Methods

auth.siws.getNonce(input)

Request a nonce and pre-built message for signing.

const { nonce, message } = await auth.siws.getNonce({
  address: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
  cluster: 'mainnet-beta', // optional, uses server default
})

Input:

FieldTypeRequiredDescription
addressstringYesSolana wallet address
clusterCluster?NoSolana cluster override

Response:

FieldTypeDescription
noncestringServer-generated nonce
messagestringComplete SIWS message ready for signing

auth.siws.verify(input)

Verify a signature and create a session.

const result = await auth.siws.verify({
  message,
  signature: bs58.encode(signatureBytes),
  address: walletAddress,
  cluster: 'mainnet-beta',
  email: 'user@example.com', // optional
})
 
if (result.success) {
  // User is authenticated, session cookie is set
  console.log(result.user)
}

Input:

FieldTypeRequiredDescription
messagestringYesThe signed SIWS message
signaturestringYesBase58-encoded signature
addressstringYesSolana wallet address
clusterCluster?NoCluster override
emailstring?No*User email (*required if anonymous: false)

Response:

FieldTypeDescription
successbooleanWhether verification succeeded
tokenstring?Session token
userobject?{ id, address, cluster }

Endpoints

The plugin registers two API endpoints:

MethodPathDescription
POST/api/auth/siws/nonceGenerate nonce and message
POST/api/auth/siws/verifyVerify and create session

Database Schema

The plugin creates a solanaAddress table to store linked wallet addresses:

ColumnTypeDescription
idstringPrimary key
userIdstringForeign key to user.id (cascade delete)
addressstringSolana wallet address (indexed)
clusterstringSolana cluster
isPrimarybooleanWhether this is the primary address
createdAtdateWhen the address was linked

Run your better-auth database migrations to create this table.

Full Example

auth.ts
auth.ts
import { betterAuth } from 'better-auth'
import { siws } from '@siws/better-auth'
 
export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    siws({
      statement: 'Sign in to MyApp',
      expirationTime: 300,
      cluster: 'mainnet-beta',
    }),
  ],
})
auth-client.ts
auth-client.ts
import { createAuthClient } from 'better-auth/client'
import { siwsClient } from '@siws/better-auth/client'
 
export const auth = createAuthClient({
  plugins: [siwsClient()],
})
connect-wallet.tsx
connect-wallet.tsx
import { auth } from './auth-client'
import bs58 from 'bs58'
 
async function signInWithSolana(wallet: { publicKey: PublicKey; signMessage: (msg: Uint8Array) => Promise<Uint8Array> }) {
  const address = wallet.publicKey.toBase58()
 
  // 1. Get nonce and message from server
  const { message } = await auth.siws.getNonce({
    address,
    cluster: 'mainnet-beta',
  })
 
  // 2. Sign with wallet
  const signatureBytes = await wallet.signMessage(
    new TextEncoder().encode(message)
  )
 
  // 3. Verify and create session
  const result = await auth.siws.verify({
    message,
    signature: bs58.encode(signatureBytes),
    address,
  })
 
  if (result.success) {
    console.log('Signed in as:', result.user)
  }
}