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
}
]
}
- Key required: include
?key=YOUR_SITE_API_KEYor headerx-bc-key: YOUR_SITE_API_KEY. - Auto-create: the directory for that key and the collection file are created automatically on first POST/GET.
- Everyone with that key can read/write/delete that key’s collections.
Endpoints
| Method | Path | Body | Response |
|---|---|---|---|
| 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)
- Go to
/homeand log in. - In “Your sites”, find a site and click API keys.
- From the modal:
- Create key – generates a new key like
myslug._1sQ4kR9JpKxz. - Copy – copy a key to the clipboard.
- Revoke – mark a key as revoked (no longer accepted by the server).
- Create key – generates a new key like
- Use the copied key as
YOUR_SITE_API_KEYin all client code.
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.
- Slug must match
^[a-z0-9-]{1,48}$. - Name is globally unique (case-insensitive). Conflicts return HTTP 409.
// 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
| Route | Serves | Notes |
|---|---|---|
/ |
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
| Variable | Default | Description |
|---|---|---|
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.