Sigma Auth
Setup

Quick Setup

The fastest way to add Sigma Auth to your Next.js app:

bun add @sigma-auth/client-plugin better-auth
.env.local
NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wif
app/api/auth/callback/route.ts
import { 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

lib/auth.ts
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)

lib/auth-store.ts
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

app/login/page.tsx
"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

app/callback/page.tsx
"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

components/auth-provider.tsx
"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

app/layout.tsx
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

app/dashboard/page.tsx
"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:

middleware.ts
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

.env.local
# Required
NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wif

# Optional
NEXT_PUBLIC_SIGMA_AUTH_URL=https://auth.sigmaidentity.com

What 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