Documentation Index
Fetch the complete documentation index at: https://docs.neynar.com/llms.txt
Use this file to discover all available pages before exploring further.
Authentication & Signer Management
Although titled “Mini app authentication”, this can also be used in web apps if you’d like.
Overview
The authorization system is built around signers - cryptographic keys that allow your application to act on behalf of a user within the Farcaster protocol.
Full code for this flow can be found in the Neynar Mini App Starter Kit
This authentication system is designed to work both in a regular web browser and inside a miniapp. In other words, it supports authentication when the miniapp context is not present (web browser) as well as when the app is running inside a miniapp. If you only need authentication for a web application, follow the Webapp flow; if you only need authentication inside a miniapp, follow the Miniapp flow.
Architecture Components
The system involves four main components:
API Endpoints
| Endpoint | Method | Purpose | Step |
|---|
/api/auth/nonce | GET | Generate authentication nonce | Step 1 |
/api/auth/signers | GET | Fetch user signers | Step 5 |
/api/auth/session-signers | GET | Fetch signers with user data | Step 5 (Miniapp) |
/api/auth/signer | POST | Create new signer | Step 7 |
/api/auth/signer | GET | Check signer status | Step 9 |
/api/auth/signer/signed_key | POST | Register signed key | Step 8 |
/api/auth/[...nextauth] | GET/POST | NextAuth handlers | Miniapp flow |
Complete Authentication Flow
Step 1: Get the Nonce
The authentication process begins by fetching a cryptographic nonce from the Neynar server.
Mini App Client → Mini App Server:
const generateNonce = async () => {
const response = await fetch('/api/auth/nonce');
const data = await response.json();
setNonce(data.nonce);
};
Mini App Server → Neynar Server:
// /api/auth/nonce/route.ts
export async function GET() {
try {
const client = getNeynarClient();
const response = await client.fetchNonce();
return NextResponse.json(response);
} catch (error) {
console.error('Error fetching nonce:', error);
return NextResponse.json(
{ error: 'Failed to fetch nonce' },
{ status: 500 }
);
}
}
Step 2: Inject Nonce in Sign in with Farcaster
The nonce is used to create a Sign in with Farcaster message.
// Webapp flow using Farcaster Auth Kit
const { signIn, connect, data } = useSignIn({
nonce: nonce || undefined,
onSuccess: onSuccessCallback,
onError: onErrorCallback,
});
// Miniapp flow using Farcaster SDK
const handleMiniappSignIn = async () => {
const result = await sdk.actions.signIn({ nonce });
// result contains message and signature
};
Step 3: Ask User for the Signature
The user is prompted to sign the SIWF message through their Farcaster client.
Webapp flow:
useEffect(() => {
if (nonce && !useMiniappFlow) {
connect(); // Triggers signing flow
}
}, [nonce, connect, useMiniappFlow]);
Miniapp flow:
// User signs through Farcaster mobile app
const signInResult = await sdk.actions.signIn({ nonce });
const { message, signature } = signInResult;
Step 4: Receive Message and Signature
Once the user signs the message, the client receives the signature.
const onSuccessCallback = useCallback(
async (res: UseSignInData) => {
console.log('✅ Authentication successful:', res);
setMessage(res.message);
setSignature(res.signature);
},
[useMiniappFlow, fetchUserData]
);
Step 5: Send to /api/auth/signers to Fetch Signers
With the signed message and signature, fetch existing signers for the user.
Mini App Client → Mini App Server:
const fetchAllSigners = async (message: string, signature: string) => {
const endpoint = useMiniappFlow
? `/api/auth/session-signers?message=${encodeURIComponent(
message
)}&signature=${signature}`
: `/api/auth/signers?message=${encodeURIComponent(
message
)}&signature=${signature}`;
const response = await fetch(endpoint);
const signerData = await response.json();
return signerData;
};
Mini App Server → Neynar Server:
// /api/auth/signers/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const message = searchParams.get('message');
const signature = searchParams.get('signature');
if (!message || !signature) {
return NextResponse.json(
{ error: 'Message and signature are required' },
{ status: 400 }
);
}
try {
const client = getNeynarClient();
const data = await client.fetchSigners({ message, signature });
return NextResponse.json({
signers: data.signers,
});
} catch (error) {
console.error('Error fetching signers:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
);
}
}
For Miniapp flow with user data:
// /api/auth/session-signers/route.ts
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const message = searchParams.get('message');
const signature = searchParams.get('signature');
const client = getNeynarClient();
const data = await client.fetchSigners({ message, signature });
const signers = data.signers;
// Fetch user data if signers exist
let user = null;
if (signers && signers.length > 0 && signers[0].fid) {
const {
users: [fetchedUser],
} = await client.fetchBulkUsers({
fids: [signers[0].fid],
});
user = fetchedUser;
}
return NextResponse.json({
signers,
user,
});
} catch (error) {
console.error('Error in session-signers API:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
);
}
}
Step 6: Check if Signers are Present
Determine if the user has existing approved signers.
const hasApprovedSigners = signerData?.signers?.some(
(signer: any) => signer.status === 'approved'
);
if (hasApprovedSigners) {
// User has signers, proceed to store them
proceedToStorage(signerData);
} else {
// No signers, need to create new ones
startSignerCreationFlow();
}
Step 7: Create a Signer
If no signers exist, create a new signer.
Mini App Client → Mini App Server:
const createSigner = async () => {
const response = await fetch('/api/auth/signer', {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to create signer');
}
return await response.json();
};
Mini App Server → Neynar Server:
// /api/auth/signer/route.ts
export async function POST() {
try {
const neynarClient = getNeynarClient();
const signer = await neynarClient.createSigner();
return NextResponse.json(signer);
} catch (error) {
console.error('Error creating signer:', error);
return NextResponse.json(
{ error: 'Failed to create signer' },
{ status: 500 }
);
}
}
Step 8: Register a Signed Key
Register the signer’s public key with the Farcaster protocol.
Mini App Client → Mini App Server:
const generateSignedKeyRequest = async (
signerUuid: string,
publicKey: string
) => {
const response = await fetch('/api/auth/signer/signed_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signerUuid,
publicKey,
redirectUrl: window.location.origin, // Optional redirect after approval
}),
});
if (!response.ok) {
throw new Error('Failed to register signed key');
}
return await response.json();
};
Mini App Server → Neynar Server:
// /api/auth/signer/signed_key/route.ts
export async function POST(request: Request) {
const body = await request.json();
const { signerUuid, publicKey, redirectUrl } = body;
// Validate required fields
if (!signerUuid || !publicKey) {
return NextResponse.json(
{ error: 'signerUuid and publicKey are required' },
{ status: 400 }
);
}
try {
// Get the app's account from seed phrase
const seedPhrase = process.env.SEED_PHRASE;
const shouldSponsor = process.env.SPONSOR_SIGNER === 'true';
if (!seedPhrase) {
return NextResponse.json(
{ error: 'App configuration missing (SEED_PHRASE)' },
{ status: 500 }
);
}
const neynarClient = getNeynarClient();
const account = mnemonicToAccount(seedPhrase);
// Get app FID from custody address
const {
user: { fid },
} = await neynarClient.lookupUserByCustodyAddress({
custodyAddress: account.address,
});
const appFid = fid;
// Generate deadline (24 hours from now)
const deadline = Math.floor(Date.now() / 1000) + 86400;
// Generate EIP-712 signature
const signature = await account.signTypedData({
domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
types: {
SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
},
primaryType: 'SignedKeyRequest',
message: {
requestFid: BigInt(appFid),
key: publicKey,
deadline: BigInt(deadline),
},
});
const signer = await neynarClient.registerSignedKey({
appFid,
deadline,
signature,
signerUuid,
...(redirectUrl && { redirectUrl }),
...(shouldSponsor && { sponsor: { sponsored_by_neynar: true } }),
});
return NextResponse.json(signer);
} catch (error) {
console.error('Error registering signed key:', error);
return NextResponse.json(
{ error: 'Failed to register signed key' },
{ status: 500 }
);
}
}
Step 9: Start Polling
Begin polling the signer status to detect when it’s approved.
const startPolling = (
signerUuid: string,
message: string,
signature: string
) => {
const interval = setInterval(async () => {
try {
const response = await fetch(`/api/auth/signer?signerUuid=${signerUuid}`);
const signerData = await response.json();
if (signerData.status === 'approved') {
clearInterval(interval);
console.log('✅ Signer approved!');
// Refetch all signers
await fetchAllSigners(message, signature);
}
} catch (error) {
console.error('Error polling signer:', error);
}
}, 2000); // Poll every 2 seconds
return interval;
};
Polling API Implementation:
// /api/auth/signer/route.ts (GET method)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const signerUuid = searchParams.get('signerUuid');
if (!signerUuid) {
return NextResponse.json(
{ error: 'signerUuid is required' },
{ status: 400 }
);
}
try {
const neynarClient = getNeynarClient();
const signer = await neynarClient.lookupSigner({
signerUuid,
});
return NextResponse.json(signer);
} catch (error) {
console.error('Error fetching signer status:', error);
return NextResponse.json(
{ error: 'Failed to fetch signer status' },
{ status: 500 }
);
}
}
Step 10: Show Signer Approval URL
Display QR code for desktop users or deep link for mobile users.
const handleSignerApproval = (approvalUrl: string) => {
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
if (isMobile && context?.client) {
// Mobile: Deep link to Farcaster app
const farcasterUrl = approvalUrl.replace(
'https://client.farcaster.xyz/deeplinks/signed-key-request',
'https://farcaster.xyz/~/connect'
);
// Use SDK to open URL in Farcaster app
sdk.actions.openUrl(farcasterUrl);
} else {
// Desktop: Show QR code
setSignerApprovalUrl(approvalUrl);
setDialogStep('access');
setShowDialog(true);
}
};
Step 11: Store Signers
Once approved, store the signers in appropriate storage.
Webapp flow (LocalStorage):
const storeInLocalStorage = (user: any, signers: any[]) => {
const authState = {
isAuthenticated: true,
user,
signers,
};
setItem(STORAGE_KEY, authState);
setStoredAuth(authState);
};
Miniapp flow (NextAuth Session):
const updateSessionWithSigners = async (signers: any[], user: any) => {
if (useMiniappFlow && message && signature && nonce) {
const signInData = {
message,
signature,
nonce,
fid: user?.fid?.toString(),
signers: JSON.stringify(signers),
user: JSON.stringify(user),
};
const result = await backendSignIn('neynar', {
...signInData,
redirect: false,
});
if (result?.ok) {
console.log('✅ Session updated with signers');
}
}
};
State Management & Storage
Webapp flow State (LocalStorage)
interface StoredAuthState {
isAuthenticated: boolean;
user: {
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
profile: {
bio: { text: string };
location?: any;
};
follower_count: number;
following_count: number;
verifications: string[];
verified_addresses: {
eth_addresses: string[];
sol_addresses: string[];
primary: {
eth_address: string;
sol_address: string;
};
};
power_badge: boolean;
score: number;
} | null;
signers: {
object: 'signer';
signer_uuid: string;
public_key: string;
status: 'approved';
fid: number;
}[];
}
// Stored in localStorage with key 'neynar_authenticated_user'
const STORAGE_KEY = 'neynar_authenticated_user';
Miniapp flow State (NextAuth Session)
interface Session {
provider: 'neynar';
user: {
fid: number;
object: 'user';
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
profile: {
bio: { text: string };
location?: any;
};
follower_count: number;
following_count: number;
verifications: string[];
verified_addresses: {
eth_addresses: string[];
sol_addresses: string[];
primary: {
eth_address: string;
sol_address: string;
};
};
power_badge: boolean;
score: number;
};
signers: {
object: 'signer';
signer_uuid: string;
public_key: string;
status: 'approved';
fid: number;
}[];
}
Security & Configuration
EIP-712 Signature Validation
// From /lib/constants.ts
export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
name: 'Farcaster SignedKeyRequestValidator',
version: '1',
chainId: 10,
verifyingContract:
'0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`,
};
export const SIGNED_KEY_REQUEST_TYPE = [
{ name: 'requestFid', type: 'uint256' },
{ name: 'key', type: 'bytes' },
{ name: 'deadline', type: 'uint256' },
];
Required Environment Variables
# Neynar API configuration
NEYNAR_API_KEY=your_neynar_api_key
NEYNAR_CLIENT_ID=your_neynar_client_id
# App signing key for signer registration
SEED_PHRASE=your_twelve_word_mnemonic_phrase
# Optional: Sponsor signer creation costs (recommended for production)
SPONSOR_SIGNER=true
Flow Detection
// Determine which flow to use
const { context } = useMiniApp();
const useMiniappFlow = context !== undefined;
// Webapp flow uses localStorage + Farcaster Auth Kit
// Miniapp flow uses NextAuth sessions + Farcaster SDK
Integration Examples
Checking Authentication Status
// Webapp flow
const isAuthenticated =
storedAuth?.isAuthenticated &&
storedAuth?.signers?.some((s) => s.status === 'approved');
// Miniapp flow
const isAuthenticated =
session?.provider === 'neynar' &&
session?.user?.fid &&
session?.signers?.some((s) => s.status === 'approved');
Using Signers for Farcaster Actions
// Get signer
const signer = (useMiniappFlow ? session?.signers : storedAuth?.signers)[0];
if (signer) {
// Use signer for publishing casts
const client = getNeynarClient();
await client.publishCast({
signerUuid: signer.signer_uuid,
text: 'Hello from my mini app!',
});
}
Summary
Authentication flow provides a comprehensive system for:
- Secure Authentication: Using SIWF protocol with cryptographic nonces
- Signer Management: Creating and approving signers for Farcaster actions
- Multi-Platform Support: Works in web browsers and Farcaster mobile clients
- State Persistence: Maintains authentication across sessions
The system abstracts the complexity of Farcaster protocol interactions while providing a seamless user experience for both web and mobile environments.
Feel Free to reach out with any questions or for further assistance in integrating this authentication flow into your Farcaster mini app!
Happy coding! 🎉