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.
| Approach | Use it when |
|---|---|
| Server-side fetch, with caching | Your app has a backend: any framework, any language |
| Build-time pull | A static site, or a single-page app with no backend |
| Webhooks | You 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:
| Header | Value |
|---|---|
ETag | W/"<hash of the body>" |
Cache-Control | private, max-age=60, stale-while-revalidate=300 |
Vary | Authorization |
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
| Content | Route |
|---|---|
| Blog or doc by path | GET /api/blogs/path?path=folder/slug (docs the same) |
| Blog or doc tree | GET /api/blogs/folders/tree |
| FAQ group by slug | GET /api/faqs/collections/slug/:slug (/faqs for its questions) |
| Testimonial | GET /api/testimonials or GET /api/testimonials/:id |
| Rich-content block | GET /api/rich-content/slug/:slug |
| Collection data | GET /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.jsonIt 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.
| Event | When |
|---|---|
blog.published | A blog post is published |
blog.updated | A published post is edited (draft edits send nothing) |
blog.unpublished | A post goes back to draft |
blog.deleted | A post is deleted |
doc.published, doc.updated, doc.unpublished, doc.deleted | The same, for docs |
faq.updated | A question is created, edited or deleted |
faq_collection.updated | A group is created, edited, archived or reordered |
testimonial.updated, testimonial.deleted | A testimonial changes; archiving counts as deleted |
rich_content.updated, rich_content.deleted | A rich-content block changes |
collection.version_published | A new collection version goes live |
collection.record_created, collection.record_updated, collection.record_deleted | A 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
- Content overview - the content types and their routes.
- Webhooks - endpoints, events, and signature checks.