Sign In With Farcaster (SIWF) Plugin for Better Auth
The `better-auth-siwf` package provides a plugin for the `better-auth` framework, enabling users to authenticate via Farcaster identities. It mirrors the developer experience of the official Sign In With Ethereum (SIWE) plugin, adapting the authentication flows and schema for Farcaster. This plugin handles the server-side verification of Farcaster Quick Auth JWTs, establishes Better Auth session cookies, and can optionally resolve enriched Farcaster user data via a provided callback (e.g., from Neynar API). The current stable version is 1.0.28. While a specific release cadence isn't stated, the 1.0.x versioning suggests a focus on stability with incremental improvements. Its key differentiator is its seamless integration with the `better-auth` ecosystem, offering a standardized approach to Farcaster authentication within that framework.
Common errors
-
TypeError: authClient.signInWithFarcaster is not a function
cause The `siwfClient()` plugin was not added to `createAuthClient`, or the client instance was not correctly type-augmented with `SIWFClientType`.fixEnsure `createAuthClient({ plugins: [siwfClient()] })` is configured and that `export const authClient = client as typeof client & SIWFClientType;` is used. -
Farcaster JWT verification failed: Invalid domain
cause The `hostname` configured in the server-side `siwf` plugin does not match the `domain` provided during Farcaster Quick Auth on the client.fixVerify that `siwf({ hostname: 'your.domain.com' })` on the server and the domain parameter used when obtaining the Farcaster JWT on the client (e.g., `miniappSdk.quickAuth.getToken({ domain: 'your.domain.com' })`) are identical. -
Error: Missing NEYNAR_API_KEY environment variable (or similar API key error from `resolveFarcasterUser`)
cause The `resolveFarcasterUser` callback, if implemented to use an external API like Neynar, requires an API key that was not provided or was incorrect.fixSet the `NEYNAR_API_KEY` (or equivalent) environment variable on your server where `better-auth` is running, or ensure the key is correctly passed to your API calls within `resolveFarcasterUser`.
Warnings
- gotcha The `hostname` configured in the server-side `siwf` plugin MUST exactly match the `domain` used when obtaining the JWT via Farcaster Quick Auth on the client. A mismatch will cause JWT verification to fail.
- gotcha The client-side `createAuthClient` configuration requires `fetchOptions: { credentials: 'include' }` to ensure that the session cookie set by the `better-auth-siwf` plugin on the server is correctly sent and received by the browser.
- gotcha When augmenting the `authClient` type with `SIWFClientType`, ensure you use `import { type SIWFClientType } from 'better-auth-siwf'` with the `type` keyword. Omitting `type` can lead to bundling issues or unexpected behavior in some environments.
- gotcha The `resolveFarcasterUser` callback is optional but highly recommended for enriching the user profile with details like username, display name, and avatar URL from a Farcaster-compatible API (e.g., Neynar). Without it, only basic FID and address information will be available.
Install
-
npm install better-auth-siwf -
yarn add better-auth-siwf -
pnpm add better-auth-siwf
Imports
- siwf
const { siwf } = require('better-auth-siwf')import { siwf } from 'better-auth-siwf' - siwfClient
const { siwfClient } = require('better-auth-siwf')import { siwfClient } from 'better-auth-siwf' - ResolveFarcasterUserResult
import { ResolveFarcasterUserResult } from 'better-auth-siwf'import { type ResolveFarcasterUserResult } from 'better-auth-siwf' - SIWFClientType
import { SIWFClientType } from 'better-auth-siwf'import { type SIWFClientType } from 'better-auth-siwf'
Quickstart
import { betterAuth } from "better-auth";
import { type ResolveFarcasterUserResult, siwf } from "better-auth-siwf";
import { createAuthClient } from "better-auth/react";
import { siwfClient, type SIWFClientType } from "better-auth-siwf";
// --- Server-side configuration (e.g., auth.ts) ---
const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY ?? ''; // Replace with actual env var or error handling
const auth = betterAuth({
// ... your better-auth config
plugins: [
siwf({
hostname: "app.example.com", // Crucial: must match the domain used in Farcaster Quick Auth
allowUserToLink: false,
resolveFarcasterUser: async ({
fid,
}): Promise<ResolveFarcasterUserResult | null> => {
if (!NEYNAR_API_KEY) {
console.warn("Neynar API key not set. Farcaster user details will not be resolved.");
return null;
}
try {
const response = await fetch(
`https://api.neynar.com/v2/farcaster/user/bulk/?fids=${fid}`,
{
method: "GET",
headers: {
"x-api-key": NEYNAR_API_KEY,
"Content-Type": "application/json",
},
}
);
const data = await response.json();
if (!data || data.users.length === 0) {
return null;
}
const user = data.users[0];
return {
fid,
username: user.username,
displayName: user.display_name,
avatarUrl: user.pfp_url,
custodyAddress: user.custody_address,
verifiedAddresses: {
primary: {
ethAddress: user.verified_addresses.primary?.eth_address ?? undefined,
solAddress: user.verified_addresses.primary?.sol_address ?? undefined,
},
ethAddresses: user.verified_addresses?.eth_addresses ?? undefined,
solAddresses: user.verified_addresses?.sol_addresses ?? undefined,
},
} satisfies ResolveFarcasterUserResult;
} catch (error) {
console.error("Error resolving Farcaster user with Neynar:", error);
return null;
}
},
}),
],
});
// --- Client-side configuration (e.g., auth-client.ts) ---
// Assume miniappSdk is available in a Farcaster MiniApp environment
declare const miniappSdk: { quickAuth: { getToken: () => Promise<{ token: string }> }, context: Promise<{ user: any; client: { clientFid: string; notificationDetails?: any } }> };
const client = createAuthClient({
plugins: [siwfClient()],
fetchOptions: {
credentials: "include", // Required for session cookies
},
});
export const authClient = client as typeof client & SIWFClientType;
// --- Client-side usage (e.g., in a React component) ---
async function signIn() {
try {
// 1) Obtain a Farcaster JWT token on the client
const result = await miniappSdk.quickAuth.getToken(); // result: { token: string }
// 2) Verify and sign in with the Better Auth server
const ctx = await miniappSdk.context;
const { data } = await authClient.signInWithFarcaster({
token: result.token,
user: {
...ctx.user,
notificationDetails: ctx.client.notificationDetails
? [
{
...ctx.client.notificationDetails,
appFid: (await miniappSdk.context).client.clientFid
}
]
: [],
}
});
if (data.success) {
console.log("Signed in successfully! User:", data.user);
// Redirect or update UI
} else {
console.error("Farcaster sign-in failed:", data.error);
}
} catch (error) {
console.error("An error occurred during Farcaster sign-in:", error);
}
}
signIn(); // Call this function from your client-side logic