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)- User connects their wallet and signs a SIWE message
- BetterAuth verifies the signature and creates a session
- The client requests a JWT from BetterAuth's
/tokenendpoint - The JWT is forwarded as
Authorization: Bearer <jwt>on REST API calls to the indexer - 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'sverifyMessage)bearer— enables Bearer token authentication on requestsjwt— issues JWTs with a 24-hour expiration containingsub(user ID) andaddress(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 byaddress + chainIdcreateMessage— 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 theset-auth-tokenresponse headersignOut— clears the session, stored token, and cached JWT
Auth Client
apps/web/lib/auth-client.ts manages token storage:
- Session tokens are stored in
localStorageundercurator_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/tokenendpoint
Environment Variables
| Variable | Purpose |
|---|---|
BETTER_AUTH_SECRET | Secret used to sign sessions and tokens |
BETTER_AUTH_URL | Base URL of the BetterAuth server (defaults to http://localhost:3000) |
NEXT_PUBLIC_APP_DOMAIN | Domain 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
| Operation | Auth Required |
|---|---|
| Read strategies (GraphQL) | No |
| Read drafts (public/unlisted) | No |
| Create/edit drafts | Yes |
| Post comments | Yes |
| Add reactions | Yes |
| Read notifications | Yes |
| Read private drafts | Yes (owner only) |