Authenticate users via Farcaster using Better Auth. This plugin mirrors the developer experience of the official SIWE plugin while adapting flows and schema to Farcaster identities.
- Server plugin:
siwf - Client plugin:
siwfClient - REST endpoints:
POST /siwf/verify
References: see the official SIWE plugin docs for structure and expectations and an earlier community attempt for Farcaster-specific ideas: SIWE Plugin Docs, it's also an expansion of this other plugin Farcaster Auth Plugin.
npm i better-auth-siwfAdd the SIWF plugin to your Better Auth configuration.
// auth.ts
import { betterAuth } from "better-auth";
import { type ResolveFarcasterUserResult, siwf } from "better-auth-siwf";
const auth = betterAuth({
// ... your better-auth config
plugins: [
siwf({
hostname: "app.example.com",
allowUserToLink: false,
// Optional: resolve the user data from neynar for example
// see neynar docs: https://docs.neynar.com/reference/fetch-bulk-users
resolveFarcasterUser: async ({
fid,
}): Promise<ResolveFarcasterUserResult | null> => {
const data = await fetch(
`https://api.neynar.com/v2/farcaster/user/bulk/?fids=${fid}`,
{
method: "GET",
headers: {
"x-api-key": process.env.NEYNAR_API_KEY,
"Content-Type": "application/json",
},
},
).then(async (data) => await data.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;
},
}),
],
});- Exposes
POST /siwf/signinto signin a Farcaster Quick Auth JWT and establish a Better Auth session cookie. - Creates a
userif one does not exist, associates it with afarcasterrecord. - Sets a secure session cookie with
SameSite: "none"for Farcaster MiniApp compatibility.
Add the client plugin so the Better Auth client exposes SIWF endpoints.
// auth-client.ts
import { createAuthClient } from "better-auth/react";
import { siwfClient, type SIWFClientType } from "better-auth-siwf";
const client = createAuthClient({
plugins: [siwfClient()],
fetchOptions: {
credentials: "include", // Required for session cookies
},
});
// Type the client to include custom farcaster methods
export const authClient = client as typeof client & SIWFClientType;Use Farcaster Quick Auth (within a Farcaster MiniApp) to obtain a signed JWT for your domain. Ensure the domain used here matches the server plugin domain.
const result = await miniappSdk.quickAuth.getToken(); // result: { token: string }Send the token and user details to the Better Auth server. On success, the Better Auth session cookie is set.
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
}
]
: [],
}
});
// data.success === true
// data.user -> { id, fid, name, image }All together:
import { sdk as miniappSdk } from "@farcaster/miniapp-sdk";
import { authClient } from "@/lib/auth-client";
const farcasterSignIn = async () => {
const isInMiniapp = await miniappSdk.isInMiniApp();
if (!isInMiniapp) {
return;
}
const ctx = await miniappSdk.context;
// 1. Obtain a Farcaster JWT token on the client
const result = await miniappSdk.quickAuth.getToken();
if (!result || !result.token) {
throw new Error("Failed to get token");
}
// 2. Verify and sign in with the Better Auth server
const { data } = await authClient.siwf.verifyToken({
token: result.token,
user: {
...ctx.user
notificationDetails: ctx.client.notificationDetails
? [
{
...ctx.client.notificationDetails,
appFid: (await miniappSdk.context).client.clientFid
}
]
: [],
}
});
if (!data.success) {
throw new Error("Failed to verify token");
}
console.log("Signed in", data.user);
};Server options accepted by siwf:
domain(string, required): Domain expected in the Farcaster JWT. Must match exactly.schema(optional): Extend or override the default plugin schema via Better AuthmergeSchema.resolveFarcasterUser(function): Resolve a Farcaster user via farcaster hubs.
Client plugin siwfClient has no options; it exposes the plugin namespace in the Better Auth client.
This plugin merges the following tables into your Better Auth schema.
| Field | Type | Notes |
|---|---|---|
| userId | string | References user.id (required) |
| fid | number | Unique Farcaster ID (required) |
| username | string | Optional |
| displayName | string | Optional |
| avatarUrl | string | Optional |
| notificationDetails | json | Optional (MiniApp notification array) |
| createdAt | date | Required |
| updatedAt | date | Required |
| Field | Type | Notes |
|---|---|---|
| userId | string | References user.id (required) |
| address | string | The wallet address (required) |
| chainId | number | The chain ID (required) |
| isPrimary | boolean | Whether the address is primary (required) |
| createdAt | date | Required |
Use the Better Auth CLI to migrate or generate schema:
npx @better-auth/cli migrate
# or
npx @better-auth/cli generateAlternatively, add the fields manually based on the tables above.
- The server verifies Farcaster JWTs with the configured
domain. Mismatched domains will fail. - Session cookies are set with
secure: true,httpOnly: true, andsameSite: "none"for MiniApp compatibility. Serve over HTTPS. - The plugin @farcaster/quick-auth ensures the JWT
sub(subject) matches the providedfidbefore issuing a session.
- 401 "Invalid Farcaster user": The JWT subject must equal the provided
fid. - No session cookie set: In embedded contexts (MiniApps), ensure third-party cookies are allowed and your server uses HTTPS with
SameSite: none. - Domain mismatch: The JWT must be issued for the same
domainconfigured in the plugin.
- Structure and schema patterns inspired by the official Better Auth SIWE plugin: SIWE Plugin Docs
- Community exploration for Farcaster auth flows: Community Farcaster Auth Plugin