Sigma Auth
Setup

Package Exports

The plugin provides multiple entry points for different use cases:

ExportUse Case
@sigma-auth/better-auth-plugin/clientBrowser-side OAuth flow, signing, encryption
@sigma-auth/better-auth-plugin/serverServer-side token exchange (any TS backend)
@sigma-auth/better-auth-plugin/nextNext.js App Router helpers
@sigma-auth/better-auth-plugin/providerAuth server plugin (for self-hosting)
@sigma-auth/better-auth-pluginType definitions only

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:

  1. Client plugin redirects to /oauth2/authorize (our custom gate)
  2. Gate checks: session → local backup → cloud backup → signup
  3. If wallet not accessible, prompts user to unlock it
  4. Once wallet is accessible, forwards to /api/auth/oauth2/authorize (Better Auth)
  5. 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 signup

All 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-auth

2. Set Environment Variables

.env.local
# 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-wif

3. Create Callback Route

app/api/auth/callback/route.ts
import { createCallbackHandler } from "@sigma-auth/better-auth-plugin/next";

export const POST = createCallbackHandler();

That's it. The plugin handles PKCE, state validation, and token exchange automatically.

Client-Side Setup

Create an 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, sigma } = authClient;

Sign In Button

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

app/callback/page.tsx
"use client";

import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { authClient } from "@/lib/auth";

function CallbackContent() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const handleCallback = async () => {
      try {
        // Plugin handles: state verification, token exchange, bapId extraction
        const result = await authClient.sigma.handleCallback(searchParams);

        // Parse bap JSON string if present
        const bapData = result.user?.bap
          ? typeof result.user.bap === "string"
            ? JSON.parse(result.user.bap)
            : result.user.bap
          : null;

        // bap_id is the direct claim from plugin
        const bapId = result.user?.bap_id || bapData?.id;

        // Store user in your state management (Zustand, Redux, etc.)
        // Example: setUser({ ...result.user, bapId }, result.access_token);

        router.push("/dashboard");
      } catch (err) {
        console.error("OAuth callback error:", err);
        setError(err instanceof Error ? err.message : "Authentication failed");
      }
    };

    handleCallback();
  }, [searchParams, router]);

  if (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

  1. User clicks sign in - Plugin generates PKCE code_verifier, stores in sessionStorage
  2. Redirects to Sigma - User authenticates, selects BAP identity, grants consent
  3. Sigma redirects back - Returns authorization code to your callback page
  4. Client-side page calls your API - Sends code + code_verifier
  5. Your API route exchanges tokens - Plugin signs request with your member key
  6. 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.

Client-Side Signing

After authentication, you can sign requests and data using the user's BAP identity. Keys never leave Sigma's domain - signing happens via a secure iframe.

Sign API Requests

// Sign a request for authenticated API calls
const authToken = await authClient.sigma.sign("/api/droplits", { name: "test" });

fetch("/api/droplits", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Auth-Token": authToken,
  },
  body: JSON.stringify({ name: "test" }),
});

Sign Bitcoin Transactions (AIP)

// Sign OP_RETURN data with Author Identity Protocol
const signedOps = await authClient.sigma.signAIP(["6a", "..."]);

Encrypt/Decrypt Messages

// Encrypt data for a friend (Type42 key derivation)
const encrypted = await authClient.sigma.encrypt(
  "Hello friend!",
  friendBapId,
  friend.publicKey
);

// Decrypt data from a friend
const decrypted = await authClient.sigma.decrypt(
  encryptedContent,
  friendBapId,
  friend.publicKey
);

// Get derived public key for friend requests
const myPubKeyForFriend = await authClient.sigma.getFriendPublicKey(friendBapId);

Identity Management

// Get current identity
const bapId = authClient.sigma.getIdentity();

// Manually set identity (for multi-identity wallets)
authClient.sigma.setIdentity(bapId);

// Clear identity on logout
authClient.sigma.clearIdentity();

// Check if ready for signing
const ready = authClient.sigma.isReady();

Custom Configuration

Override defaults if needed:

app/api/auth/callback/route.ts
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

.env.local
# 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.com

Other TypeScript Backends (Elysia, Hono, Express)

For non-Next.js backends, use exchangeCodeForTokens directly from the /server export:

Elysia example
import { Elysia, t } from "elysia";
import { exchangeCodeForTokens } from "@sigma-auth/better-auth-plugin/server";

const app = new Elysia()
  .post("/oauth/exchange", async ({ body, set }) => {
    const { code, redirectUri, codeVerifier } = body;

    try {
      const result = await exchangeCodeForTokens({
        code,
        redirectUri,
        clientId: process.env.SIGMA_CLIENT_ID!,
        memberPrivateKey: process.env.SIGMA_MEMBER_PRIVATE_KEY!,
        codeVerifier,
        // Optional: override issuer URL
        // issuerUrl: "https://auth.sigmaidentity.com",
      });

      return {
        access_token: result.access_token,
        id_token: result.id_token,
        refresh_token: result.refresh_token,
        user: result.user,
      };
    } catch (error) {
      const err = error as { status?: number; error?: string };
      set.status = err.status || 500;
      return { error: err.error || "Token exchange failed" };
    }
  }, {
    body: t.Object({
      code: t.String(),
      redirectUri: t.String(),
      codeVerifier: t.Optional(t.String()),
    }),
  });

Server Export API

import {
  exchangeCodeForTokens,
  type TokenExchangeOptions,
  type TokenExchangeResult,
  type TokenExchangeError,
} from "@sigma-auth/better-auth-plugin/server";

TokenExchangeOptions:

  • code - Authorization code from OAuth callback
  • redirectUri - Must match the redirect_uri used in authorization
  • clientId - Your OAuth client ID
  • memberPrivateKey - Your member private key (WIF format)
  • codeVerifier - PKCE code verifier (optional)
  • issuerUrl - Auth server URL (default: https://auth.sigmaidentity.com)

TokenExchangeResult:

  • user - SigmaUserInfo with BAP identity
  • access_token - OAuth access token
  • id_token - OIDC ID token
  • refresh_token - Refresh token (optional)

Common Mistakes

Using Wrong Env Var Name

# WRONG
MEMBER_PRIVATE_KEY=...
DROPLIT_MEMBER_PRIVATE_KEY=...

# CORRECT
SIGMA_MEMBER_PRIVATE_KEY=...

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

On this page