Sigma Auth
Setup

Quick Setup

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

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

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

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

signIn.sigma uses /auth/sigma/callback as the default callbackURL, so you only need to pass clientId.

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

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

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 type="button" onClick={signOut} className="mt-4 px-4 py-2 bg-red-600 text-white rounded">
        Sign Out
      </button>
    </div>
  );
}

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

On this page