better-auth-siwf
v1.0.28
Published
better-auth plugin for Sign In With Farcaster
Maintainers
Readme
Better Auth – Sign In With Farcaster (SIWF)
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.
Installation
npm i better-auth-siwfServer Setup
Add 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;
},
}),
],
});What the plugin does
- 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.
Client Setup
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;Usage
1) Obtain a Farcaster JWT token on the client
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 }2) Verify and sign in
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);
};Configuration Options
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.
Database Schema
This plugin merges the following tables into your Better Auth schema.
farcaster
| 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 |
walletAddress
| 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 |
Migrations
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.
Security Notes
- 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.
Troubleshooting
- 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.
Acknowledgements
- 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
