Architecture: Fronting Better Auth
The Sigma Auth client plugin intentionally fronts Better Auth's OIDC authorize endpoint. This is a deliberate architectural decision to ensure wallet access is a prerequisite to authentication.
Why We Front the Endpoint
Standard OAuth providers (Better Auth included) handle authentication in a straightforward way: check session, show consent, return authorization code. But Sigma Identity requires something more:
Wallet access must be verified before any OAuth flow completes.
Better Auth doesn't know about Bitcoin wallets, encrypted backups, or BAP identities. So we intercept the OAuth flow:
- Client plugin redirects to
/oauth2/authorize(our custom gate) - Gate checks: session → local backup → cloud backup → signup
- If wallet not accessible, prompts user to unlock it
- Once wallet is accessible, forwards to
/api/auth/oauth2/authorize(Better Auth) - Better Auth handles standard OAuth (session, consent, authorization code)
This makes Bitcoin identity the foundation of every authentication, not just an add-on.
The Gate Logic
Client App → /oauth2/authorize (custom gate)
↓
1. Check session cookie
├─ Yes → Proceed to Better Auth
↓
2. Check localStorage (encrypted backup)
├─ Yes → Show password prompt
↓
3. Check cloud backup availability
├─ Yes → Redirect to /restore
↓
4. No backup → Redirect to signupAll paths are logged for debugging and audit purposes.
Quick Start (Next.js)
Add Sigma Auth to your Next.js app in 3 steps:
1. Install
bun add @sigma-auth/better-auth-plugin better-auth
# or
npm install @sigma-auth/better-auth-plugin better-auth2. Set Environment Variables
# Your OAuth client ID (register at auth.sigmaidentity.com/account)
NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
# Your app's member private key for signing token exchange requests
SIGMA_MEMBER_PRIVATE_KEY=your-member-wif3. Create Callback Route
import { createCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
export const runtime = "nodejs";
export const POST = createCallbackHandler();That's it. The plugin handles PKCE, state validation, and token exchange automatically.
Client-Side Setup
Create an 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, sigma } = authClient;Sign In Button
"use client";
import { signIn } from "@/lib/auth";
export function SignInButton() {
const handleSignIn = () => {
signIn.sigma({
clientId: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID!,
callbackURL: `${window.location.origin}/callback`,
});
};
return <button onClick={handleSignIn}>Sign in with Sigma</button>;
}Callback Page
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
function CallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();
const [status, setStatus] = useState<"loading" | "error">("loading");
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");
setStatus("error");
return;
}
try {
// Exchange code for tokens via your API route
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();
// Store session (use your preferred state management)
localStorage.setItem("auth_session", JSON.stringify({ user, access_token }));
sessionStorage.removeItem("sigma_code_verifier");
router.push("/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "Authentication failed");
setStatus("error");
}
};
handleCallback();
}, [searchParams, router]);
if (status === "error") {
return (
<div className="p-4">
<h2>Authentication Error</h2>
<p>{error}</p>
<button onClick={() => router.push("/")}>Return Home</button>
</div>
);
}
return <div>Completing authentication...</div>;
}
export default function CallbackPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<CallbackContent />
</Suspense>
);
}How It Works
- User clicks sign in - Plugin generates PKCE code_verifier, stores in sessionStorage
- Redirects to Sigma - User authenticates, selects BAP identity, grants consent
- Sigma redirects back - Returns authorization code to your callback page
- Client-side page calls your API - Sends code + code_verifier
- Your API route exchanges tokens - Plugin signs request with your member key
- Returns user profile + tokens - Includes BAP identity, pubkey, etc.
The member private key (SIGMA_MEMBER_PRIVATE_KEY) proves your app's identity to the Sigma Auth server during token exchange. This is generated when you register your OAuth client.
Features
Automatic PKCE
Code challenge/verifier handled automatically for secure public client flows.
State Validation
CSRF protection via state parameter.
Bitcoin Auth Signatures
Token exchange requests are signed with your member key using bitcoin-auth.
BAP Identity Support
User profiles include full BAP identity data when available.
Custom Configuration
Override defaults if needed:
import { createCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
export const runtime = "nodejs";
export const POST = createCallbackHandler({
// Override Sigma Auth URL
issuerUrl: "https://custom.sigmaidentity.com",
// Override client ID
clientId: "custom-client-id",
// Override member key
memberPrivateKey: process.env.CUSTOM_MEMBER_KEY,
// Override callback path for redirect URI
callbackPath: "/auth/callback",
});Environment Variables
# Required
NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
SIGMA_MEMBER_PRIVATE_KEY=your-member-wif
# Optional (defaults shown)
NEXT_PUBLIC_SIGMA_AUTH_URL=https://auth.sigmaidentity.comCommon Mistakes
Using Wrong Env Var Name
# WRONG
MEMBER_PRIVATE_KEY=...
DROPLIT_MEMBER_PRIVATE_KEY=...
# CORRECT
SIGMA_MEMBER_PRIVATE_KEY=...API Route Without Runtime
// WRONG - Needs nodejs runtime for bitcoin-auth crypto
export const POST = createCallbackHandler();
// CORRECT
export const runtime = "nodejs";
export const POST = createCallbackHandler();Missing Suspense for useSearchParams
// WRONG
export default function CallbackPage() {
const searchParams = useSearchParams(); // Error!
}
// CORRECT - Wrap in Suspense
function CallbackContent() {
const searchParams = useSearchParams();
}
export default function CallbackPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<CallbackContent />
</Suspense>
);
}Next Steps
- Register OAuth Client - Get your client ID and member key
- OAuth Error Handling - Handle common errors
- PKCE Flow Details - Understand the security model