Brandfine Docs
SDK

Caching

createCache and createKeyedCache — server-side SWR primitives.

The SDK ships two server-side cache primitives. They live in the consumer site's memory (per SSR process) and refresh via the publish webhook. Stale-while-revalidate semantics out of the box.

import { createCache, createKeyedCache } from '@brandfine/client/cache'

Single-slot — createCache

For resources that don't vary by any key (workspace metadata).

const workspaceCache = createCache({
  label: 'workspace',
  ttl: 24 * 60 * 60 * 1000, // 24h safety net
  fetch: () => bf.workspace.get(),
})

const ws = await workspaceCache.get()
workspaceCache.invalidate() // called from webhook

Keyed — createKeyedCache

For per-key dimensions: per-locale posts, per-nav-key navigations, anything where the same fetcher returns different data for different inputs.

const postsCache = createKeyedCache({
  label: 'posts',
  ttl: 60 * 60 * 1000,
  fetch: (locale) => bf.posts.list({ type: 'blog', locale }),
})

await postsCache.get('en')
await postsCache.get('pt')
postsCache.invalidate('en')   // one key
postsCache.invalidate()       // all keys

The contract

ConditionBehavior
Fresh (not dirty + within TTL)Serve cached.
Cold start (no data)Block on fresh fetch.
Invalidated (webhook)Block on fresh fetch — not stale-while-revalidate.
TTL-expired with cached dataStale-while-revalidate. Serve stale + bg refresh.
Refetch error with cached dataLog via onError, serve stale (stale-while-error).
Concurrent reads during in-flightDedupe — all await the same promise.

The invalidate-blocks-fresh rule is the important one: when the webhook fires, the next page render must see the new data, not the old. Otherwise editors would need to refresh twice to see their changes.

Custom error logger

const cache = createCache({
  label: 'workspace',
  ttl: 24 * 60 * 60 * 1000,
  fetch: () => bf.workspace.get(),
  onError: (err) => logger.warn({ err }, 'workspace refetch failed'),
})

Defaults to console.warn if omitted.

Caching adapted shapes

When the API's BrandfinePost doesn't match what your page templates expect, adapt at the cache boundary:

type Service = {
  slug: string
  title: string
  applyUrl: string | null
}

function adapt(post: BrandfinePost<ServiceConfig>): Service {
  return {
    slug: post.slug,
    title: post.title,
    applyUrl: post.customConfig?.apply?.url ?? null,
  }
}

const servicesCache = createKeyedCache<Service[]>({
  label: 'services',
  ttl: 60 * 60 * 1000,
  fetch: async (locale) =>
    (await bf.posts.list({ type: 'services', locale })).map(adapt),
})

On this page