Skip to main content
If your app calls the API when nobody is looking at it, from a worker, a cron job or a queue, it has to keep refresh tokens. The SDK gives you the pieces to do that safely. It provides an encryption layer, a token store that refreshes without races, your own session tokens for your routes, and filters that keep tokens and personal data out of your logs. You need a database you can write an adapter for and a 32-byte encryption key. Everything here imports from the core entrypoint, @fanvue/builder-sdk, unless a sample says otherwise.

Encrypt secrets at rest

createFanvueCrypto({ currentKey, previousKeys }) returns AES-256-GCM encryption over a key registry. currentKey is kid:base64key, for example v2:<32 bytes base64>. Bare key material without a prefix takes the key id v1 (DEFAULT_KEY_ID). previousKeys is either null or a comma-separated list of retired kid:base64key entries, which can decrypt but never encrypt. withPurpose returns a SecretCipher with only encrypt and decrypt, which is the type the token store and webhook receiver take. parseKeyRegistry(config) validates the configuration without encrypting anything. Every failure is a CryptoError with one of the codes CRYPTO_KEY_INVALID, CRYPTO_KEY_UNKNOWN, CRYPTO_MALFORMED_CIPHERTEXT, CRYPTO_DECRYPT_FAILED or CRYPTO_ENCRYPT_FAILED. Error messages never contain key material or plaintext.
lib/crypto.ts

Store tokens

The token store keeps one encrypted TokenSet per subject, where the subject is your own stable id for the creator. You implement TokenStorageAdapter over your database, and the SDK owns the refresh logic. createTokenStoreContext({ adapter, cipher, oauth }) binds the adapter, a SecretCipher and your OAuthConfig, and wires the SDK’s own refresh call. Nothing in the store throws. A null from getAccessToken means one of these, and in every case you should show the creator a reconnect state:
  • the subject never connected
  • the subject has no refresh token
  • the record can’t be decrypted after a key rotation that dropped the old key
  • the refresh was refused
Persist tokens as they arrive from the session exchange, then read them from a worker. Note the throw inside onTokens: storeTokenSet never throws, so without it a creator would receive a session whose tokens were never stored.
lib/token-store.ts
app/api/fanvue/session/route.ts
workers/daily-digest.ts

Issue your own app sessions

createAppSessions(config) signs and verifies the bearer JWTs your creator surface and fan surface send to your routes. These are the SDK’s own session tokens, not Fanvue tokens. They carry identity claims and no access token, which is why they can live longer than a Fanvue access token. CreatorSessionClaimsSchema carries typ: 'creator', subjectId, fanvueUserUuid, displayName and handle. FanSessionClaimsSchema carries typ: 'fan', fanvueFanUuid, experienceUuid, entitled: true and preview. Extend either with .extend({ ... }) to add app claims. Never make it .strict(), because a verified payload also carries iss, aud, sub, jti, iat and exp.
lib/sessions.ts
The result exposes signCreatorSession, signFanSession, getCreatorSession(authorizationHeader) and getFanSession(authorizationHeader). A creator token presented to the fan verifier does not verify, because the audiences differ. Every verification failure returns null. The @fanvue/builder-sdk/nextjs/embedded-app entrypoint builds on these sessions:
  • createCreatorSessionHandler and createFanSessionHandler are the bootstrap routes that exchange Fanvue’s tokens for app sessions.
  • requireCreatorSession, requireFanSession and requireCreatorAccessToken guard your routes. Each returns { ok: true, session }, or { ok: false, response } with the response ready to return.
  • requireCreatorAccessToken also fetches a live Fanvue token through the token store, and answers 401 fanvue_reconnect_required when none exists.
app/api/courses/route.ts
isSessionCurrent is an optional revocation check against your own database. A check that throws counts as revoked.

Protect internal endpoints

requireMachineAuth(request, options) authenticates a cron tick or queue drain against your own endpoints. It isn’t a way to authenticate to Fanvue. It tries two strategies in order: a shared bearer secret compared in constant time, then an app-supplied SignedRequestVerifier.
app/api/cron/refresh/route.ts
boundedBatchSize(requested, defaultSize, max) clamps an operator-supplied ?limit= to a positive integer no greater than max. Pass a SignedRequestVerifier with the exact request bytes in rawBody when a queue provider signs its deliveries.

Scrub logs and error reports

createSafeLogFields(extraAllowedKeys?) returns a filter that:
  • drops every field not on an allowlist
  • drops values that are not string | number | boolean | null
  • replaces any string containing a UUID or an email address with [redacted-uuid] or [redacted-email]
BASE_ALLOWED_LOG_KEYS holds cause, code, creatorId, errorName, httpStatus, reason, retryCount and status.
lib/log.ts
errorName(error) returns only the class name, never error.message, which routinely carries upstream bodies and signed URLs. logEvent is a ready-made logger over the base allowlist with the prefix [fanvue]. createSentryScrubber(extraSensitiveKeyPattern?) returns a beforeSend function for an error-reporting client, with no dependency on one. It redacts values under keys matching BASE_SENSITIVE_KEY_PATTERN, which covers authorization, cookie, token, secret, password, signedurl, api_key, credential, email, phone and ip_address. It also strips UUIDs, emails and URL query strings from every string, and stops its walk at depth 32. configurationReadiness(env?) returns two named checks for a readiness route. The detail names missing variables, never their values.

Secrets checklist

  • SESSION_SECRET of at least 32 random characters. createConfig accepts shorter values, but createAppSessions and requireMachineAuth don’t.
  • Encryption keys of exactly 32 bytes, base64 encoded, with the key id prefix.
  • Client secret, encryption keys and CRON_SECRET on the server only. The browser receives the session JWT and nothing else.
  • Webhook signing secrets encrypted with their own purpose, as in Receive webhooks with the SDK.

See also