Brandfine Docs
Concepts

Static sites

How to use the Brandfine SDK from a site with no server runtime (Astro static export, Hugo, plain HTML, WordPress).

The Brandfine SDK is server-side only in v1 — your workspace API key is broad-scope and should never reach a browser bundle. For sites with a server runtime (Next.js, Remix, Nuxt, Astro SSR, SvelteKit, Express), that's straightforward: every recipe in the SDK docs does it.

If your site has no server runtime, you have three options.

Every modern static-site framework supports a "hybrid" mode where most pages stay static but a few selected routes render at request time. This is the cleanest path and what we recommend.

Astro

// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'

export default defineConfig({
  output: 'static',
  adapter: node({ mode: 'standalone' }),
})

Then mark specific pages/routes as on-demand:

---
// src/pages/api/contact.ts
export const prerender = false  // ← this route renders on every request

import { createBrandfineClient } from '@brandfine/client'

const bf = createBrandfineClient({
  baseUrl: 'https://api.brandfine.co',
  apiKey: import.meta.env.BRANDFINE_API_KEY,
})

export const POST: APIRoute = async ({ request }) => {
  await bf.submissions.create(/* ... */)
  // ...
}
---

Static pages stay static. The /api/contact route runs on a tiny Node server. Your hosting changes from "static file CDN" to "static file CDN + Node runtime" — most platforms (Netlify, Vercel, Cloudflare Pages, Railway, Fly.io) make this a one-click switch.

Next.js

Already SSR by default. If you're on output: 'export' for a fully-static deploy, drop that line and most platforms will deploy your route handlers as serverless functions automatically.

Other frameworks

  • Eleventy / Hugo / Jekyll: pair with a small Node/Bun/Deno proxy on a separate domain (e.g. api.yourdomain.com) — see Option 2 below.
  • WordPress: install the Brandfine WordPress plugin. It ships the WP REST proxy + booking widget + analytics auto-inject, no custom PHP needed.
  • Webflow / Squarespace / Wix: built-in form handling, then webhook the data into Brandfine via a serverless function.

Option 2 — Tiny Node proxy on its own domain

If switching your main site to hybrid isn't appealing, run a small Node server (~30 lines) on a sibling domain. Your static site posts to it; it proxies to Brandfine.

// server.ts (Hono example — works on Node, Bun, Deno, Workers)
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { createBrandfineClient } from '@brandfine/client'

const bf = createBrandfineClient({
  baseUrl: 'https://api.brandfine.co',
  apiKey: process.env.BRANDFINE_API_KEY!,
})

const app = new Hono()

app.use('/*', cors({ origin: ['https://yourdomain.com'] }))

app.post('/contact', async (c) => {
  const body = await c.req.json()
  const created = await bf.submissions.create(body)
  return c.json(created)
})

app.post('/appointments', async (c) => {
  const body = await c.req.json()
  const created = await bf.appointments.createRequest(body)
  return c.json(created)
})

export default app

Deploy this to Railway, Fly.io, a small VPS, or Cloudflare Workers. Cost: a few dollars a month for low-traffic sites. Trade-off: one more service to maintain.

Option 3 — Wait for publishable keys

We're working on scoped publishable keys (bfpk_*) — the two-key model Stripe, GitHub, and Mapbox all use. A publishable key would be:

  • Safe to embed directly in your static HTML / JS bundle
  • Capability-gated to a specific allowlist of endpoints (e.g. appointments:write only)
  • Independent revocation from your server-side key
  • Per-key rate limiting for spam resistance

Until they ship, Options 1 and 2 are the supported paths. We'll update this page with a "It's live!" notice when they're ready.

If you're blocked specifically by this and would prefer to wait rather than refactor your site, email hello@brandfine.co so we can prioritize.

Why we don't just let you put the broad-scope key in HTML

The workspace API key (bfwk_*) you get from the CMS today reads posts, navigations, contact-form submissions, analytics, AND can write submissions + appointments. It's the equivalent of a Stripe secret key — meant for server-side use only.

Putting it in HTML wouldn't expose anything currently sensitive (everything /external/* exposes is either already public content or a write-only ingest endpoint), but:

  1. It blanket-allows any read/write capability we add in the future — including ones that might be sensitive
  2. Per-IP rate limiting becomes essential against spam, which the server doesn't apply to broad-scope keys today
  3. Revoking the key takes down all your server-side integrations along with the embed
  4. Secret scanners can't tell the difference between "this is safe in browser" vs "this should never be public" without the prefix distinction

We'd rather take the short-term inconvenience of Option 1/2 than ship a permanent footgun. Publishable keys will resolve this properly.

On this page