Quick Setup
The fastest way to add Sigma Auth to your Next.js app:
bun add @sigma-auth/better-auth-plugin better-authNEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wifimport { createCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
export const POST = createCallbackHandler();The plugin POSTs the code to /api/auth/sigma/callback and uses /auth/sigma/callback as the OAuth redirect_uri by default — put your route and callback page at those paths and the whole flow works with zero config. See the Better Auth Plugin Setup for complete details, including the full list of subpath exports (/client, /server, /next, /provider).
Complete Example
Project Structure
app/
api/
auth/
sigma/
callback/
route.ts # Token exchange endpoint
auth/
sigma/
callback/
page.tsx # Client-side callback handler
dashboard/
page.tsx # Protected page
login/
page.tsx # Login page
layout.tsx
lib/
auth.ts # Auth client setup
auth-store.ts # State management (zustand)Auth Client
import { createAuthClient } from "better-auth/client";
import { sigmaClient } from "@sigma-auth/better-auth-plugin/client";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_SIGMA_AUTH_URL || "https://auth.sigmaidentity.com",
plugins: [sigmaClient()],
});
export const { signIn } = authClient;State Management (Zustand)
import { create } from "zustand";
interface User {
id: string;
pubkey?: string;
name?: string;
avatar?: string;
bap?: unknown;
}
interface AuthState {
user: User | null;
accessToken: string | null;
isAuthenticated: boolean;
isHydrated: boolean;
setUser: (user: User, token: string) => void;
signOut: () => void;
loadFromStorage: () => void;
}
const STORAGE_KEY = "auth_session";
export const useAuthStore = create<AuthState>((set) => ({
user: null,
accessToken: null,
isAuthenticated: false,
isHydrated: false,
setUser: (user, token) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ user, accessToken: token }));
set({ user, accessToken: token, isAuthenticated: true });
},
signOut: () => {
localStorage.removeItem(STORAGE_KEY);
set({ user: null, accessToken: null, isAuthenticated: false });
},
loadFromStorage: () => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const { user, accessToken } = JSON.parse(stored);
set({ user, accessToken, isAuthenticated: true, isHydrated: true });
} else {
set({ isHydrated: true });
}
} catch {
set({ isHydrated: true });
}
},
}));Login Page
signIn.sigma uses /auth/sigma/callback as the default callbackURL, so you only need to pass clientId.
"use client";
import { signIn } from "@/lib/auth";
import { Button } from "@/components/ui/button";
const SIGMA_CLIENT_ID = process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID ?? "";
export default function LoginPage() {
const handleSignIn = () => {
signIn.sigma({ clientId: SIGMA_CLIENT_ID });
};
return (
<div className="flex min-h-screen items-center justify-center">
<div className="space-y-4 text-center">
<h1 className="text-2xl font-bold">Sign In</h1>
<Button onClick={handleSignIn} size="lg">
Sign in with Sigma Identity
</Button>
</div>
</div>
);
}Callback Page
The plugin exposes authClient.sigma.handleCallback(searchParams) which handles PKCE, state verification, and the token-exchange round-trip to /api/auth/sigma/callback in one call. useSearchParams must be wrapped in <Suspense> per the Next.js App Router rules.
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { authClient } from "@/lib/auth";
import { useAuthStore } from "@/lib/auth-store";
function CallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();
const setUser = useAuthStore((s) => s.setUser);
const [error, setError] = useState("");
useEffect(() => {
authClient.sigma
.handleCallback(searchParams)
.then((result) => {
setUser(result.user, result.access_token);
router.push("/dashboard");
})
.catch((err: Error) => setError(err.message));
}, [searchParams, router, setUser]);
if (error) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-xl font-semibold text-red-600">Authentication Error</h2>
<p className="mt-2">{error}</p>
<button type="button" onClick={() => router.push("/login")} className="mt-4 underline">
Try Again
</button>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center">
<p>Completing authentication...</p>
</div>
);
}
export default function CallbackPage() {
return (
<Suspense fallback={<p>Loading...</p>}>
<CallbackContent />
</Suspense>
);
}Auth Provider
"use client";
import { useEffect } from "react";
import { useAuthStore } from "@/lib/auth-store";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const loadFromStorage = useAuthStore((s) => s.loadFromStorage);
const isHydrated = useAuthStore((s) => s.isHydrated);
useEffect(() => {
loadFromStorage();
}, [loadFromStorage]);
if (!isHydrated) {
return null; // or loading spinner
}
return <>{children}</>;
}Layout
import { AuthProvider } from "@/components/auth-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}Protected Page
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useAuthStore } from "@/lib/auth-store";
export default function DashboardPage() {
const router = useRouter();
const { user, isAuthenticated, signOut } = useAuthStore();
useEffect(() => {
if (!isAuthenticated) {
router.push("/login");
}
}, [isAuthenticated, router]);
if (!user) return null;
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<div className="space-y-2">
<p>Welcome, {user.name || user.pubkey}</p>
{user.bap && <p>BAP ID: {(user.bap as { idKey: string }).idKey}</p>}
</div>
<button type="button" onClick={signOut} className="mt-4 px-4 py-2 bg-red-600 text-white rounded">
Sign Out
</button>
</div>
);
}Environment Variables
# Required
NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wif
# Optional
NEXT_PUBLIC_SIGMA_AUTH_URL=https://auth.sigmaidentity.comWhat You Get
After authenticating, the user object includes:
{
sub: "user-id",
pubkey: "0x...", // Bitcoin public key
name: "User Name",
picture: "https://...",
email: "user@example.com",
bap: {
idKey: "bap-identity-key",
currentAddress: "1...",
addresses: [...],
// ... full BAP identity
}
}Next Steps
- Register OAuth Client - Get credentials for your app
- BAP Profiles - Understanding Bitcoin Attestation Protocol
- Error Handling - Handle authentication errors