Setup
Quick Setup
The fastest way to add Sigma Auth to your Next.js app:
bun add @sigma-auth/client-plugin better-authNEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wifimport { createCallbackHandler } from "@sigma-auth/client-plugin/next";
export const runtime = "nodejs";
export const POST = createCallbackHandler();See the Client Plugin Setup for complete details.
Complete Example
Project Structure
app/
api/
auth/
callback/
route.ts # Token exchange endpoint (4 lines)
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/client-plugin";
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
"use client";
import { signIn } from "@/lib/auth";
import { Button } from "@/components/ui/button";
export default function LoginPage() {
const handleSignIn = () => {
signIn.sigma({
clientId: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID!,
callbackURL: `${window.location.origin}/callback`,
});
};
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
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
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(() => {
const handleCallback = async () => {
const code = searchParams.get("code");
const codeVerifier = sessionStorage.getItem("sigma_code_verifier");
if (!code) {
setError("No authorization code");
return;
}
try {
const response = await fetch("/api/auth/callback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, code_verifier: codeVerifier }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Token exchange failed");
}
const { user, access_token } = await response.json();
setUser(user, access_token);
sessionStorage.removeItem("sigma_code_verifier");
router.push("/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "Authentication failed");
}
};
handleCallback();
}, [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 onClick={() => router.push("/login")} className="mt-4 underline">
Try Again
</button>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4" />
<p>Completing authentication...</p>
</div>
</div>
);
}
export default function CallbackPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<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 onClick={signOut} className="mt-4 px-4 py-2 bg-red-600 text-white rounded">
Sign Out
</button>
</div>
);
}Middleware (Optional)
For server-side route protection:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Public routes
if (pathname.startsWith("/login") || pathname.startsWith("/callback") || pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
// Protected routes - check client-side in the page component
// Middleware can't access localStorage, so just let client handle it
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};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 Identity - Understanding Bitcoin Attestation Protocol
- Error Handling - Handle authentication errors