BuildBaseBuildBase

Delivering content

Show blogs, docs, FAQs and other content in any stack, fetched on your server or at build time and cached, never from the browser.

Content is read with an org API token, and that token is a secret: it must stay on your server, in a build step, or in CI. There is no browser read path for blogs, docs, FAQs, testimonials, rich content or collection data, and that is on purpose. A page that fetched from BuildBase on every visit would put its traffic on us instead of on your cache.

There are three supported ways to show content. None of them is tied to a framework or a language.

ApproachUse it when
Server-side fetch, with cachingYour app has a backend: any framework, any language
Build-time pullA static site, or a single-page app with no backend
WebhooksYou cache content and want it refreshed the moment it changes

The token is orgId:secret, created under Settings → API keys, and sent as Authorization: Bearer <token>. Give it the built-in content-reader role: it can read blogs, docs, FAQs, testimonials, rich content, collections and forms, and nothing else. A token created without a role has full admin access, which a site that only renders content never needs.

Server-side fetch, with caching

Fetch from a route, a server component, a controller, a loader: anywhere that runs on your server. Then cache the result, so one fetch serves many page views.

Content responses carry these headers:

HeaderValue
ETagW/"<hash of the body>"
Cache-Controlprivate, max-age=60, stale-while-revalidate=300
VaryAuthorization

Keep the ETag with what you cached and send it back as If-None-Match. If nothing changed, the answer is an empty 304 Not Modified: you keep serving your copy. BuildBase answers repeated reads from its own cache, so a 304 does no database work at all, and any write to that content type clears the cache immediately. private means the response answered a secret: keep it in your server's cache, not a shared CDN.

That Cache-Control value is what an org API token receives. The console, signed in as a person who has just edited the content, gets private, no-cache instead, so it always revalidates and never shows a list from before the edit.

# First fetch: keep the body and the ETag.
curl -i "https://api.console.buildbase.app/api/rich-content/slug/refund-policy" \
  -H "Authorization: Bearer $BUILDBASE_API_TOKEN"

# Later: ask only whether it changed.
curl -i "https://api.console.buildbase.app/api/rich-content/slug/refund-policy" \
  -H "Authorization: Bearer $BUILDBASE_API_TOKEN" \
  -H 'If-None-Match: W/"5f2c…"'

The same in Python with requests, caching in memory for five minutes and revalidating after that:

import os, time, requests

BASE = "https://api.console.buildbase.app"
TOKEN = os.environ["BUILDBASE_API_TOKEN"]
_cache = {}  # path -> (fetched_at, etag, body)

def content(path, max_age=300):
    hit = _cache.get(path)
    if hit and time.time() - hit[0] < max_age:
        return hit[2]
    headers = {"Authorization": f"Bearer {TOKEN}"}
    if hit:
        headers["If-None-Match"] = hit[1]
    r = requests.get(BASE + path, headers=headers, timeout=5)
    if r.status_code == 304:
        _cache[path] = (time.time(), hit[1], hit[2])
        return hit[2]
    r.raise_for_status()
    body = r.json()
    _cache[path] = (time.time(), r.headers.get("ETag", ""), body)
    return body

policy = content("/api/rich-content/slug/refund-policy")

Any HTTP client can do this: fetch, Go's net/http, PHP, Ruby. In JavaScript and TypeScript, the SDK's server client does it for you, with a pluggable cache (memory, Redis, KV) and webhook handling. It runs on Node, Bun, Deno, Workers and edge runtimes, and refuses to load in a browser so the token cannot end up in a bundle. It is a convenience, not a requirement: everything on this page works without it.

import { createServerClient } from '@buildbase/sdk/server';

const content = createServerClient({
  serverUrl: 'https://api.console.buildbase.app',
  apiToken: process.env.BUILDBASE_API_TOKEN!,
});

const posts = await content.blogs.list();
const policy = await content.richContent.get('refund-policy');
const faq = await content.faqs.questions('help-center');

Read routes for sites

ContentRoute
Blog or doc by pathGET /api/blogs/path?path=folder/slug (docs the same)
Blog or doc treeGET /api/blogs/folders/tree
FAQ group by slugGET /api/faqs/collections/slug/:slug (/faqs for its questions)
TestimonialGET /api/testimonials or GET /api/testimonials/:id
Rich-content blockGET /api/rich-content/slug/:slug
Collection dataGET /api/collections/data/:slug?latest=true

The content-reads quota

Content reads answered with a body count toward your plan's monthly content reads. A 304 Not Modified is never counted and never refused, so an app that revalidates costs almost nothing, however much traffic it serves. An app that refetches the full body on every page load is the one that uses up its allowance.

Build-time pull

A static site, or a single-page app that has no server of its own, cannot hold a token at runtime, so it should not fetch at runtime. Pull the content while the site builds instead: the token lives in CI, the content is written into the build as files, and visitors never reach BuildBase.

In JavaScript projects the SDK ships a command for this:

BUILDBASE_SERVER_URL=https://api.console.buildbase.app \
BUILDBASE_API_TOKEN=$BUILDBASE_API_TOKEN \
npx @buildbase/sdk content pull --out content/buildbase.json

It writes one JSON file with every published blog, doc, FAQ group, testimonial and rich-content block; add --collections slug1,slug2 for collection data and --modules blogs,docs to pull less. Once the SDK is a dependency, the same command is npx buildbase content pull.

In any other stack, a build step that calls the same endpoints with curl and writes the JSON is the whole technique. Pair it with a deploy hook, which rebuilds the site when you start a deploy from the console, or with a webhook that triggers a rebuild so the site updates when content changes.

Webhooks: refresh on change

Rather than polling, subscribe a webhook endpoint to the content events and clear your cache, or trigger your site's rebuild, when one arrives.

EventWhen
blog.publishedA blog post is published
blog.updatedA published post is edited (draft edits send nothing)
blog.unpublishedA post goes back to draft
blog.deletedA post is deleted
doc.published, doc.updated, doc.unpublished, doc.deletedThe same, for docs
faq.updatedA question is created, edited or deleted
faq_collection.updatedA group is created, edited, archived or reordered
testimonial.updated, testimonial.deletedA testimonial changes; archiving counts as deleted
rich_content.updated, rich_content.deletedA rich-content block changes
collection.version_publishedA new collection version goes live
collection.record_created, collection.record_updated, collection.record_deletedA record changes, including a public form submission

Deliveries use the same headers and signature as every other BuildBase webhook: x-buildbase-signature: sha256=<hex>, the HMAC-SHA256 of {timestamp}.{body} with your endpoint's signing secret, and x-buildbase-timestamp. See Webhooks for verifying it in any language.

Every content event's data names what changed, plus ready-made cache tags:

{
  "module": "blog",
  "id": "66f1…",
  "slug": "launch-week",
  "path": null,
  "tags": [
    "buildbase:blog",
    "buildbase:blog:launch-week",
    "buildbase:blog:id:66f1…"
  ]
}

buildbase:<module> covers every list of that module; the slug and id tags cover a single item. path is reserved and null for now. If you tag your cache entries the same way, a webhook handler only has to delete the tags it is given. In JavaScript, handleContentWebhook from @buildbase/sdk/server verifies the delivery and clears those tags from the client's cache, and toNodeHandler mounts it in Express, Fastify or NestJS (it needs the raw body, hence express.raw):

import {
  createContentWebhookHandler,
  createServerClient,
  toNodeHandler,
} from '@buildbase/sdk/server';
import express from 'express';

const content = createServerClient({
  serverUrl: 'https://api.console.buildbase.app',
  apiToken: process.env.BUILDBASE_API_TOKEN!,
});

const app = express();
app.post(
  '/webhooks/buildbase',
  express.raw({ type: 'application/json' }),
  toNodeHandler(
    createContentWebhookHandler({
      secret: process.env.BUILDBASE_WEBHOOK_SECRET!,
      cache: content.cache,
    })
  )
);

Next Steps