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
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
| Option | Type | Default | Description |
|---|---|---|---|
statement | string? | -- | Statement shown when signing |
expirationTime | number? | 300 | Expiration in seconds |
cluster | Cluster? | 'mainnet-beta' | Default Solana cluster |
resources | string[]? | -- | Resource URIs to include |
emailDomainName | string? | 'siws.local' | Domain for synthetic emails |
anonymous | boolean? | true | Allow sign-up without email |
schema | object? | -- | Custom solanaAddress table schema |
Client Plugin
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:
| Field | Type | Required | Description |
|---|---|---|---|
address | string | Yes | Solana wallet address |
cluster | Cluster? | No | Solana cluster override |
Response:
| Field | Type | Description |
|---|---|---|
nonce | string | Server-generated nonce |
message | string | Complete 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:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The signed SIWS message |
signature | string | Yes | Base58-encoded signature |
address | string | Yes | Solana wallet address |
cluster | Cluster? | No | Cluster override |
email | string? | No* | User email (*required if anonymous: false) |
Response:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether verification succeeded |
token | string? | Session token |
user | object? | { id, address, cluster } |
Endpoints
The plugin registers two API endpoints:
| Method | Path | Description |
|---|---|---|
POST | /api/auth/siws/nonce | Generate nonce and message |
POST | /api/auth/siws/verify | Verify and create session |
Database Schema
The plugin creates a solanaAddress table to store linked wallet addresses:
| Column | Type | Description |
|---|---|---|
id | string | Primary key |
userId | string | Foreign key to user.id (cascade delete) |
address | string | Solana wallet address (indexed) |
cluster | string | Solana cluster |
isPrimary | boolean | Whether this is the primary address |
createdAt | date | When the address was linked |
Run your better-auth database migrations to create this table.
Full Example
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
import { createAuthClient } from 'better-auth/client'
import { siwsClient } from '@siws/better-auth/client'
export const auth = createAuthClient({
plugins: [siwsClient()],
})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)
}
}