support.eth curator

Authentication

SIWE-based authentication — how the web app signs users in and the indexer verifies requests

Authentication

Curator Studio uses Sign-In With Ethereum (SIWE) via BetterAuth for authentication. The system has two parts: the web app handles wallet login and session management, while the indexer verifies tokens on protected API calls.

All read operations are public. Authentication is only required for write operations — creating drafts, posting comments, adding reactions, and reading notifications.

Auth Flow

Wallet → SIWE challenge → BetterAuth → Session + JWT

                                         Bearer token


                               Indexer (JWKS verification)
  1. User connects their wallet and signs a SIWE message
  2. BetterAuth verifies the signature and creates a session
  3. The client requests a JWT from BetterAuth's /token endpoint
  4. The JWT is forwarded as Authorization: Bearer <jwt> on REST API calls to the indexer
  5. The indexer verifies the JWT against BetterAuth's JWKS public keys — no shared secret needed

Web App Setup

BetterAuth Server

BetterAuth is configured in apps/web/lib/auth.ts with three plugins:

  • siwe — handles nonce generation and SIWE message verification (via viem's verifyMessage)
  • bearer — enables Bearer token authentication on requests
  • jwt — issues JWTs with a 24-hour expiration containing sub (user ID) and address (wallet address) claims
export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
  secret: process.env.BETTER_AUTH_SECRET,
  database: process.env.DATABASE_URL
    ? new Pool({ connectionString: process.env.DATABASE_URL })
    : new Database("./auth.db"),
  plugins: [
    siwe({ /* ... */ }),
    bearer(),
    jwt({
      jwt: {
        expirationTime: "24h",
        definePayload: async ({ user }) => ({
          sub: user.id,
          address: user.name?.toLowerCase(),
        }),
      },
    }),
  ],
});

SIWE Adapter

The RainbowKit SIWE adapter in apps/web/lib/siwe-adapter.ts bridges wallet signing with BetterAuth:

  • getNonce — requests a nonce from BetterAuth keyed by address + chainId
  • createMessage — builds a SIWE message with the statement "Sign in to Curator Studio"
  • verify — sends the signed message to BetterAuth for verification; the session token is captured automatically via the set-auth-token response header
  • signOut — clears the session, stored token, and cached JWT

Auth Client

apps/web/lib/auth-client.ts manages token storage:

  • Session tokens are stored in localStorage under curator_auth_token
  • JWTs are cached for 23 hours (1-hour buffer before the 24-hour expiry) under curator_jwt
  • getJWT() returns the cached JWT or fetches a fresh one from BetterAuth's /token endpoint

Environment Variables

VariablePurpose
BETTER_AUTH_SECRETSecret used to sign sessions and tokens
BETTER_AUTH_URLBase URL of the BetterAuth server (defaults to http://localhost:3000)
NEXT_PUBLIC_APP_DOMAINDomain for SIWE message verification (defaults to localhost:3000)

SDK Integration

Token Sync

The web app syncs the JWT into the SDK via a CuratorAuthSync component:

function CuratorAuthSync() {
  const { sdk, setAuthToken } = useCuratorSDK();
  const { address } = useAccount();

  useEffect(() => {
    async function syncToken() {
      const sessionToken = getStoredToken();
      if (!sessionToken || !address) {
        setAuthToken(null);
        return;
      }
      const jwt = await getJWT();
      setAuthToken(jwt);
    }
    syncToken();
  }, [sdk, address, setAuthToken]);

  return null;
}

setAuthToken stores the token on the indexer instance so all SDK extensions (drafts, comments, reactions, notifications) can access it.

Authenticated Requests

SDK extensions use a shared getAuthHeader helper to attach the token to REST calls:

function getAuthHeader(indexer: Indexer): Record<string, string> {
  const token = indexer.getAuthToken();
  return token ? { Authorization: `Bearer ${token}` } : {};
}

This header is included on every write request to the indexer's REST API.

Indexer Middleware

JWT verification lives in packages/indexer/src/api/middleware.ts. It uses the jose library with a remote JWKS:

const JWKS = createRemoteJWKSet(
  new URL(`${AUTH_URL}/api/auth/jwks`)
);

export async function verifyJWT(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: AUTH_URL,
    audience: AUTH_URL,
  });
  return { address: payload.address };
}

The requireAuth Hono middleware extracts the Bearer token from the Authorization header, verifies it, and sets the caller's address on the request context. Protected routes access c.get("address") to identify the authenticated user.

The indexer fetches public keys from BetterAuth's /.well-known/jwks.json endpoint. The AUTH_URL environment variable on the indexer must point to the web app's BetterAuth instance.

Protected Endpoints

OperationAuth Required
Read strategies (GraphQL)No
Read drafts (public/unlisted)No
Create/edit draftsYes
Post commentsYes
Add reactionsYes
Read notificationsYes
Read private draftsYes (owner only)

On this page