Quickstart

All examples assume:
- Your HTML/JS is served by the same BuzzCloud server (same origin)
- You have created an API key for one of your sites in the dashboard

Replace YOUR_SITE_API_KEY with an actual key from the API keys panel in /home.

// basic setup
const KEY = 'YOUR_SITE_API_KEY';        // e.g. "myslug._1sQ4kR9JpKxz"
const COLL = 'chat_global';

// list items
fetch(`/api/db/${encodeURIComponent(COLL)}?key=${encodeURIComponent(KEY)}`)
  .then(r => r.json())
  .then(console.log);

// create item
fetch(`/api/db/${encodeURIComponent(COLL)}?key=${encodeURIComponent(KEY)}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: 'Guest', text: 'hello' })
}).then(r => r.json()).then(console.log);

// update item
fetch(`/api/db/${encodeURIComponent(COLL)}/ITEM_ID?key=${encodeURIComponent(KEY)}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text: 'edited' })
}).then(r => r.json());

// delete item
fetch(`/api/db/${encodeURIComponent(COLL)}/ITEM_ID?key=${encodeURIComponent(KEY)}`, {
  method: 'DELETE'
}).then(r => r.json());

Public Realtime Database /api/db/:collection

Global JSON collections stored on disk under DATABASE_ROOT. With per-site keys, the on-disk layout is:

DATABASE_ROOT/
  <apiKey>/
    <collection>.json

Each collection file looks like:

{
  "items": [
    {
      "id": "a1b2c3...",
      "yourField": "...",
      "created_at": 1730500000000,
      "updated_at": 1730500000000
    }
  ]
}

Endpoints

MethodPathBodyResponse
GET /api/db/<collection>?key=YOUR_SITE_API_KEY { items: [...] }
POST /api/db/<collection>?key=YOUR_SITE_API_KEY JSON object (your fields) Created item with id, created_at, updated_at
PUT /api/db/<collection>/<id>?key=YOUR_SITE_API_KEY Partial JSON Updated item
DELETE /api/db/<collection>/<id>?key=YOUR_SITE_API_KEY { ok:true } or { error:"Not found" }

API Keys (per site) /api/sites/:id/keys

Each published site can have one or more API keys. These keys are used for both /api/db/* and /api/rt/subscribe.

Creating and managing keys (UI)

API (if you are building admin tooling)

These routes require you to be logged in (same auth as the Sites API).

// list keys for a site
GET /api/sites/:id/keys

// create a new key for a site
POST /api/sites/:id/keys
-> { id, site_id, slug, api_key, created_at, revoked_at: null }

// revoke a key
DELETE /api/sites/:id/keys/:keyId
-> { ok: true }

API keys are tied to the site/slug, but you can use the key from any page or app that talks to the same BuzzCloud backend.

SSE Realtime /api/rt/subscribe

Subscribe to change notifications for a single collection via Server-Sent Events (EventSource).

// Browser example
const KEY  = 'YOUR_SITE_API_KEY';
const COLL = 'chat_global';

const es = new EventSource(
  `/api/rt/subscribe?collection=${encodeURIComponent(COLL)}&key=${encodeURIComponent(KEY)}`
);

// Fires once on connect
es.addEventListener('hello', (e) => {
  console.log('hello', JSON.parse(e.data));
});

// Full current list (fires after hello)
es.addEventListener('snapshot', (e) => {
  const { items } = JSON.parse(e.data);
  console.log('snapshot size', items.length);
});

// On any write to the collection (POST/PUT/DELETE)
es.onmessage = async (e) => {
  console.log('update notice', JSON.parse(e.data));
  // Typical pattern: re-fetch the list for that key
  const list = await fetch(
    `/api/db/${encodeURIComponent(COLL)}?key=${encodeURIComponent(KEY)}`
  ).then(r => r.json());
  // ...redraw UI...
};

// If EventSource errors (e.g., cross-origin or file://), fall back:
es.onerror = () => {
  console.warn('SSE failed; fallback to polling');
  setInterval(async () => {
    const list = await fetch(
      `/api/db/${encodeURIComponent(COLL)}?key=${encodeURIComponent(KEY)}`
    ).then(r => r.json());
    // ...redraw UI...
  }, 1000);
};

Tip: Serve your pages through BuzzCloud (http://your-box:3001/...). Opening as file:// will often break SSE; use the polling fallback in that case.

Use-Case Examples

1) Simple Chat Wall

Collection name: chat_global

const KEY  = 'YOUR_SITE_API_KEY';
const COLL = 'chat_global';

// Post a message
await fetch(`/api/db/${COLL}?key=${encodeURIComponent(KEY)}`, {
  method:'POST',
  headers:{'Content-Type':'application/json'},
  body: JSON.stringify({ username:'Guest', text:'Hi!' })
});

// Subscribe + re-fetch on change (see SSE section)

2) Lightweight Multiplayer State

Collection name: game_players

const KEY = 'YOUR_SITE_API_KEY';

// Create your player
const me = await fetch('/api/db/game_players?key=' + encodeURIComponent(KEY), {
  method:'POST',
  headers:{'Content-Type':'application/json'},
  body: JSON.stringify({ name:'Player', x:0, y:0 })
}).then(r=>r.json());

// Move updates while key held (throttle ~10/s)
function sendPos(x,y){
  fetch(`/api/db/game_players/${encodeURIComponent(me.id)}?key=${encodeURIComponent(KEY)}`, {
    method:'PUT',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({ x, y })
  });
}

// Subscribe to updates + re-GET game_players on message to render everyone

This is a shared public board for whoever has the key. Do not store private info or assume per-user security.

Sites API (Authenticated) /api/sites

After login (cookie bc_token), users can create/edit static pages at:
/website/<slug> and /website/<slug>.html plus nested paths like /website/<slug>/some/page.

// Create
POST /api/sites
{ "name":"My Page", "slug":"mysite", "content":"..." }

// Get list
GET /api/sites

// Get full content for one site
GET /api/sites/:id

// Update (can change name/slug/content)
PUT /api/sites/:id
{ "name?":"New name", "slug?":"new-slug", "content?":"..." }

// Delete
DELETE /api/sites/:id

These endpoints require you to be logged in via /api/auth/login. The HTTP-only cookie is sent automatically by the browser.

Auth (Dashboard) /api/auth/*

// Sign up
POST /api/auth/signup
{ "name":"User", "email":"[email protected]", "password":"supersecret123" }

// Login
POST /api/auth/login
{ "email":"[email protected]", "password":"supersecret123" }

// Current user
GET /api/auth/me

// Logout
POST /api/auth/logout

Auth is only required for dashboard / site management / key management. Public DB (/api/db/*) and realtime (/api/rt/subscribe) use API keys, not login.

Media / Streaming /movies /stream/:filename /page/:basename

// List discovered media files
GET /movies
-> [ { "filename":"clip.mp4", "url":"/stream/clip.mp4" }, ... ]

// Stream directly (Range supported)
GET /stream/clip.mp4

// Auto player page (or serves /movies/basename.html if present)
GET /page/clip

Static Routes

RouteServesNotes
/ landing.html from HOME_DIR Main landing page.
/home home.html from HOME_DIR Owner dashboard shell (sites, drafts, API keys).
/status status.html from HOME_DIR Uses /api/health/*.
/website Static sites under WEBSITES_PATH Serves index.html or home.html if present.
/website/<slug> Main file for a site’s slug Equivalent to /website/<slug>.html for top-level pages.
/website/<slug>.html The saved site file Managed via Sites API.
/website/<slug>/... Nested files under that slug directory Good for multi-page sites and assets.

Environment Variables

VariableDefaultDescription
PORT 3001 HTTP port for the Express app.
DECK_ROOT (server dir parent) Root folder for home/websites/movies/database.
HOME_DIR <DECK_ROOT>/home Where landing.html, home.html, status.html live.
WEBSITES_PATH <DECK_ROOT>/websites User site files (served under /website).
MOVIES_PATH <DECK_ROOT>/movies Audio/video files for listing/streaming.
DATABASE_ROOT <DECK_ROOT>/database Directory for JSON collections per key (<apiKey>/<collection>.json).
JWT_SECRET dev-secret-change-me Signs the bc_token cookie for Sites/API auth.
SECURE_COOKIES false Set true to mark cookies secure (HTTPS only).
DB_FILE <DECK_ROOT>/secure/buzzcloud.db SQLite file for users, sites, and API keys.

AI Usage Prompt

Paste this into another assistant so it uses the BuzzCloud API correctly. You will give it a real API key at runtime.

You are building a browser app that talks to a self-hosted backend called BuzzCloud.

Rules:
- Your frontend HTML/JS is served by the same BuzzCloud server (same origin).
- Do NOT invent new endpoints. Use ONLY the routes below.
- I will give you one BuzzCloud API key string (for example: "myslug._1sQ4kR9JpKxz"). Call this value KEY.
- For ALL /api/db/* and /api/rt/subscribe calls you MUST include that key as ?key=KEY (or header x-bc-key: KEY).

PUBLIC REALTIME DB (PER-SITE KEYS, NO LOGIN)
- The database is "public" for anyone who has the key. Do not store secrets.
- Collections are per key. Files live under DATABASE_ROOT/<KEY>/<collection>.json.

Endpoints (KEY is mandatory):
- GET    /api/db/:collection?key=KEY
        -> { items:[ { id, ...yourFields, created_at, updated_at }, ... ] }

- POST   /api/db/:collection?key=KEY
        body: JSON object with arbitrary fields
        -> created item (adds id, created_at, updated_at)

- PUT    /api/db/:collection/:id?key=KEY
        body: partial JSON
        -> updated item

- DELETE /api/db/:collection/:id?key=KEY
        -> { ok:true } or { error:"Not found" }

REALTIME (SSE)
- Subscribe to one collection with the same KEY:

  const es = new EventSource(
    '/api/rt/subscribe?collection=' + encodeURIComponent(coll) +
    '&key=' + encodeURIComponent(KEY)
  );

  es.addEventListener('hello',    (e) => { /* fires once on connect */ });
  es.addEventListener('snapshot', (e) => {
    // data: { collection, items:[...] }
  });

  es.onmessage = async (e) => {
    // A write happened (POST/PUT/DELETE) for that KEY+collection.
    // On message, refetch the list:
    const list = await fetch(
      '/api/db/' + encodeURIComponent(coll) + '?key=' + encodeURIComponent(KEY)
    ).then(r => r.json());
    // then redraw the UI from list.items
  };

- If EventSource errors (e.g. running from file:// or cross-origin), fall back to 1s polling:
  - setInterval(() => GET /api/db/:collection?key=KEY and redraw), ~1000ms.

SITES API (AUTH REQUIRED, NOT USED BY PUBLIC CLIENTS UNLESS I EXPLICITLY ASK)
- Requires login cookie (bc_token) set via:
  POST /api/auth/signup { name, email, password }
  POST /api/auth/login  { email, password }

- Once logged in, a user can manage sites:
  GET    /api/sites
  GET    /api/sites/:id
  POST   /api/sites      { name, slug, content }
  PUT    /api/sites/:id  { name?, slug?, content? }
  DELETE /api/sites/:id

- Constraints:
  - slug: ^[a-z0-9-]{1,48}$
  - name: globally unique (case-insensitive)

API KEYS FOR DB + REALTIME (MANAGED PER SITE)
- I will manage API keys via the dashboard or via:
  GET    /api/sites/:id/keys
  POST   /api/sites/:id/keys
  DELETE /api/sites/:id/keys/:keyId

- You (the assistant) should assume you are given exactly ONE KEY string.
  Never try to create or revoke keys yourself unless I explicitly request admin tooling.

MEDIA
- GET /movies
    -> [ { filename, url:'/stream/...' }, ... ]
- GET /stream/:filename
    -> media stream with HTTP Range support
- GET /page/:basename
    -> serves /movies/<basename>.html if present, otherwise a simple player page

STATIC
- /                  -> landing.html (HOME_DIR)
- /home              -> home.html (dashboard)
- /status            -> status.html
- /website           -> listing / index for sites
- /website/:slug     -> saved site (same as /website/:slug.html for top level)
- /website/:slug/... -> nested pages/assets for that site

YOUR JOB:
- ALWAYS include ?key=KEY (or header x-bc-key: KEY) on every /api/db/* and /api/rt/subscribe request.
- Prefer SSE (/api/rt/subscribe) to know when to refetch; fall back to polling if EventSource fails.
- Do NOT add custom auth or secret storage around the public DB. It is intentionally key-only and shared.
- Do NOT invent routes. If you need something that is not described above, ask me instead of guessing.