@fanvue/builder-sdk
v0.3.0
Published
OAuth authentication library for the Fanvue API
Keywords
Readme
@fanvue/builder-sdk
Authentication for Fanvue apps. Every Fanvue app is either embedded or off-platform (which should I build?) — this SDK covers both:
| Building an... | Import | What you get | Docs |
|---|---|---|---|
| Embedded App (runs inside Fanvue, in an iframe) | @fanvue/builder-sdk/nextjs/embedded-app + @fanvue/builder-sdk/react | Session-token exchange handler, useEmbeddedAuth hook, Bearer sessions with auto refresh | Overview · Integration guide |
| Off-Platform App ("Login with Fanvue" on your own domain) | @fanvue/builder-sdk/nextjs/off-platform | Full-page redirect flow, httpOnly cookie sessions, auto token refresh | Auth overview · Implementation guide |
Building something else (Node, Deno, a custom server)? The core @fanvue/builder-sdk
entrypoint exposes the low-level OAuth primitives both flows are built on.
- Drop-in route handlers for Next.js -- a couple of files and you're done
- Handles token exchange, refresh, and session management so you don't have to
- Fully typed with TypeScript
[!IMPORTANT] Renamed from
@fanvue/auth. This package was previously published as@fanvue/auth(≤ 0.2.3). The API is unchanged — to migrate, swap the dependency and update import paths:- "@fanvue/auth": "^0.2.3" + "@fanvue/builder-sdk": "^0.3.0"- import { useEmbeddedAuth } from "@fanvue/auth/react"; + import { useEmbeddedAuth } from "@fanvue/builder-sdk/react";
@fanvue/authis deprecated on npm and will receive no further releases.
Installation
npm install @fanvue/builder-sdk
# or
pnpm add @fanvue/builder-sdk
# or
yarn add @fanvue/builder-sdkPeer dependencies (next, react) are optional -- only install what your project uses.
Quick Start
Embedded App (your app inside Fanvue)
When Fanvue opens your app in an iframe, it appends a short-lived session token
to your embed URL (?token=...). Exchange it server-side for real OAuth tokens
-- the SDK handles the whole delegated flow (PKCE, state, code exchange).
1. Add the session-exchange route
// app/api/fanvue/session/route.ts
import { createConfig, createSessionExchangeHandler } from "@fanvue/builder-sdk/nextjs/embedded-app";
export const { POST } = createSessionExchangeHandler(createConfig());2. Authenticate in your embedded page
// app/embedded/page.tsx
"use client";
import { AuthProvider, useAuth, useEmbeddedAuth } from "@fanvue/builder-sdk/react";
function Embedded() {
const { status, error, theme } = useEmbeddedAuth();
const { authFetch } = useAuth();
// `theme` is the creator's active colour scheme ("light" | "dark"), read
// from the iframe URL -- use it to match Fanvue. It's `null` when the app is
// opened outside Fanvue, so fall back to a sensible default.
return (
<div data-theme={theme ?? "light"}>
{status === "exchanging" && <p>Connecting to Fanvue…</p>}
{status === "error" && <p>Auth failed: {error}</p>}
<button onClick={() => authFetch("/api/me")}>Load my profile</button>
</div>
);
}
export default function Page() {
return (
<AuthProvider>
<Embedded />
</AuthProvider>
);
}3. Call the Fanvue API from your own routes
// app/api/me/route.ts
import { NextResponse } from "next/server";
import { HEADER_UPDATED_SESSION } from "@fanvue/builder-sdk";
import { createConfig, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/embedded-app";
const config = createConfig();
export async function GET() {
const auth = await getAuthenticatedClient({ sessionSecret: config.sessionSecret, config });
if (!auth) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
const user = await auth.client.getCurrentUser();
const res = NextResponse.json(user.isOk() ? user.value : { error: "api_failed" });
// When the access token was refreshed, hand the new session JWT back to the
// client -- `authFetch` stores it automatically.
if (auth.refreshedJwt) res.headers.set(HEADER_UPDATED_SESSION, auth.refreshedJwt);
return res;
}Hosting requirements for embedded apps:
- Serve over HTTPS with a browser-trusted certificate.
- Send
Content-Security-Policy: frame-ancestors https://www.fanvue.comso Fanvue can frame your page. - The session token lives ~60 seconds -- exchange it promptly (the
useEmbeddedAuthhook does this on mount). - To act on the creator's behalf after the iframe closes, persist the
refresh token from the
onTokenshook:
export const { POST } = createSessionExchangeHandler(createConfig(), {
onTokens: async ({ tokens, user }) => {
await db.saveRefreshToken(user.uuid, tokens.refresh_token);
},
});See the embedded apps integration guide for the full walkthrough, including app registration in the Builder.
Off-Platform App ("Login with Fanvue")
The fastest path: create a config file and three route handlers.
1. Configure
// lib/auth.ts
import { createConfig } from "@fanvue/builder-sdk/nextjs/off-platform";
export const authConfig = {
...createConfig(), // reads OAUTH_* and SESSION_* env vars
afterLoginPath: "/dashboard",
afterLogoutPath: "/",
};2. Add route handlers
// app/api/oauth/login/route.ts
import { createLoginHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";
export const { GET } = createLoginHandler(authConfig);// app/api/oauth/callback/route.ts
import { createCallbackHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";
export const { GET, POST } = createCallbackHandler(authConfig);// app/api/oauth/logout/route.ts
import { createLogoutHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";
export const { POST } = createLogoutHandler(authConfig);3. Use the session
import { getSession, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";
// Read the session in any server component or route handler
const session = await getSession(authConfig.sessionSecret, authConfig.sessionCookieName);
// Or get an API client that automatically refreshes expired tokens
const client = await getAuthenticatedClient({
sessionSecret: authConfig.sessionSecret,
sessionCookieName: authConfig.sessionCookieName,
config: authConfig,
});
if (client) {
const user = await client.getCurrentUser();
if (user.isOk()) console.log(user.value);
}See the authentication docs for scopes, rate limits, and the underlying OAuth flow.
Core (advanced)
Use the core entrypoint when you need full control -- CLI tools, custom servers, or non-Next.js frameworks.
import {
createAuthorizationUrl,
exchangeCodeForToken,
refreshAccessToken,
createSessionJwt,
verifySessionJwt,
createFanvueClient,
} from "@fanvue/builder-sdk";
const config = {
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "http://localhost:3000/callback",
issuerUrl: null,
apiBaseUrl: null,
scopes: null,
responseMode: null,
prompt: null,
};
// 1. Build the authorization URL (PKCE is handled automatically)
const { url, codeVerifier, state } = await createAuthorizationUrl(config);
// Redirect the user to `url`...
// 2. Exchange the authorization code for tokens
const tokenResult = await exchangeCodeForToken(config, {
code: "code-from-callback",
codeVerifier,
redirectUri: null,
});
if (tokenResult.isErr()) throw new Error(tokenResult.error.message);
const tokens = tokenResult.value;
// 3. Make authenticated API calls
const client = createFanvueClient(tokens.access_token, null);
const userResult = await client.getCurrentUser();
if (userResult.isOk()) console.log(userResult.value);
// 4. Refresh an expired access token
const refreshResult = await refreshAccessToken(config, tokens.refresh_token);For the embedded flow, the equivalent core primitive is exchangeSessionToken(config, sessionToken),
which runs PKCE generation, the delegated authorize-on-behalf request, and the
code exchange in one call.
import { createSessionJwt, verifySessionJwt } from "@fanvue/builder-sdk";
// Create a signed session JWT (HS256, default 30-day expiry)
const jwt = await createSessionJwt("your-session-secret", {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
tokenType: tokens.token_type,
scope: tokens.scope,
idToken: tokens.id_token,
userUuid: "...",
handle: "...",
displayName: "...",
isCreator: false,
avatarUrl: null,
});
// Verify a session JWT
const session = await verifySessionJwt("your-session-secret", jwt);Environment Variables
Add these to your .env.local (Next.js) or equivalent:
# Required
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
SESSION_SECRET=at-least-32-characters-long-random-string
# Optional (shown with defaults)
OAUTH_ISSUER_BASE_URL=https://auth.fanvue.com
API_BASE_URL=https://api.fanvue.com
FANVUE_PLATFORM_URL=https://www.fanvue.com # embedded apps only
OAUTH_SCOPES="openid offline_access offline"
SESSION_COOKIE_NAME=fanvue_session
# OAUTH_RESPONSE_MODE= # query, form_post, etc.
# OAUTH_PROMPT= # login, consent, etc.For the Fanvue dev environment use https://auth.dev.fanvue.com,
https://api.dev.fanvue.com, and https://dev.fanvue.com respectively.
The createConfig() helper reads these automatically. You can also override any value programmatically:
const config = createConfig({
clientId: "explicit-id", // overrides OAUTH_CLIENT_ID
sessionCookieName: "my_session", // overrides SESSION_COOKIE_NAME
});Error Handling
All async operations return Result<T, E> types from neverthrow instead of throwing exceptions. This gives you type-safe, explicit error handling:
const result = await client.getCurrentUser();
if (result.isOk()) {
console.log(result.value); // FanvueUser
} else {
console.error(result.error); // ApiError
}
// Or use neverthrow's functional API
result
.map((user) => console.log(user.displayName))
.mapErr((err) => console.error(err.message));Error types: OAuthError (token exchange/refresh), EmbeddedAuthError (delegated authorize-on-behalf), ApiError (API requests), SessionVerifyError (JWT verification).
API Reference
Core (@fanvue/builder-sdk)
| Export | Description |
|---|---|
| createAuthorizationUrl(config, opts?) | Build a PKCE authorization URL. Returns { url, codeVerifier, state }. |
| exchangeCodeForToken(config, opts) | Exchange an authorization code for tokens. Returns Result<TokenResponse, OAuthError>. |
| refreshAccessToken(config, refreshToken) | Refresh an expired access token. Returns Result<TokenResponse, OAuthError>. |
| exchangeSessionToken(config, sessionToken) | Exchange an embedded session token for tokens (full delegated flow). Returns Result<TokenResponse, EmbeddedAuthError \| OAuthError>. |
| requestAuthorizationCodeOnBehalf(config, sessionToken, opts) | Low-level: request a delegated authorization code from the platform. |
| getSessionTokenFromUrl(url) | Extract the ?token= session token from a URL. |
| getThemeFromUrl(url) | Extract the ?theme= colour scheme from a URL. Returns 'light' \| 'dark' \| null. |
| createSessionJwt(secret, payload, expiresIn?) | Create a signed HS256 session JWT. Default expiry: 30 days. |
| verifySessionJwt(secret, token) | Verify and decode a session JWT. Returns Result<SessionPayload, SessionVerifyError>. |
| createFanvueClient(accessToken, apiBaseUrl?) | Create an authenticated API client. |
| API_VERSION | The API version header value (currently 2025-06-26). |
| HEADER_UPDATED_SESSION | Response header (X-Updated-Session) carrying a refreshed session JWT. |
Types
| Type | Description |
|---|---|
| OAuthConfig | Client ID, secret, redirect URI, and optional overrides |
| EmbeddedAuthConfig | OAuthConfig plus the Fanvue platform base URL |
| FanvueTheme | The creator's active colour scheme: 'light' \| 'dark' |
| TokenResponse | Access token, refresh token, expiry, scopes |
| SessionPayload | JWT claims: tokens, user info, expiry timestamp |
| FanvueUser | User profile (uuid, email, handle, displayName, isCreator, avatarUrl, etc.) |
| OAuthError | Error from token exchange or refresh |
| EmbeddedAuthError | Error from the delegated authorize-on-behalf flow |
| ApiError | Error from API requests |
| SessionVerifyError | Error from session JWT verification |
Next.js Embedded App (@fanvue/builder-sdk/nextjs/embedded-app)
| Export | Description |
|---|---|
| createConfig(opts?) | Build an EmbeddedAppConfig from env vars and/or explicit options (adds platformUrl) |
| createSessionExchangeHandler(config, hooks?) | { POST } -- exchanges the embedded session token for tokens, responds with a session JWT |
| getSession(secret) | Read and verify the session from the Authorization: Bearer header |
| getAuthenticatedClient(opts) | Get a FanvueClient from the Bearer session, with automatic token refresh |
| getSessionTokenFromUrl(url) | Extract the ?token= session token from a URL |
Next.js Off-Platform App (@fanvue/builder-sdk/nextjs/off-platform)
| Export | Description |
|---|---|
| createConfig(opts?) | Build a ResolvedConfig from env vars and/or explicit options |
| createLoginHandler(opts) | { GET } -- redirects to the OAuth provider |
| createCallbackHandler(opts) | { GET, POST } -- completes the code exchange, sets the session cookie |
| createLogoutHandler(opts) | { POST } -- clears the session cookie |
| getSession(secret, cookieName?) | Read and verify the session from cookies |
| getAuthenticatedClient(opts) | Get a FanvueClient with automatic token refresh |
React (@fanvue/builder-sdk/react)
| Export | Description |
|---|---|
| AuthProvider | Context provider. Manages JWT storage in sessionStorage. |
| useAuth() | Returns { jwt, isAuthenticated, setJwt, clearJwt, authFetch } |
| useEmbeddedAuth(opts?) | Returns { status, error, theme }. Exchanges the embedded session token on mount; theme is the creator's colour scheme ('light' \| 'dark' \| null). |
Examples
Complete working examples:
| Example | App type | Auth flow |
|---|---|---|
| examples/nextjs-embedded-app | Embedded | Session-token exchange (delegated authorize-on-behalf) |
| examples/nextjs-off-platform-app | Off-platform | "Login with Fanvue" full-page redirect |
Contributing
pnpm install # Install dependencies
pnpm build # Build the library
pnpm dev # Watch mode
pnpm test # Run tests
pnpm typecheck # Type-check
pnpm lint # Lint
pnpm check # Run all checks (format + lint + typecheck + test)License
MIT
