# Brisk > Drop a folder, get a site. Brisk is an open-source internal hosting platform: deploy a folder of static files and get a live URL in about a second, plus six zero-config browser APIs — database, identity, AI, file storage, realtime channels, and the hosting itself — from one script tag. Brisk is for internal use behind a login. Every site is visible and writable by every authenticated teammate; there are no permissions, no site owners, no custom backends, no build steps, no cron jobs, and no seventh primitive. Those constraints are the product. It runs on Cloudflare (one Worker + R2 + D1 + Durable Objects) or self-hosted on any Node box (SQLite plus a disk or an S3 bucket) from the same code. This file is generated at build time for Brisk 0.4.2 from the repository and the pages of the instance serving it, so it matches what is running here. Root-relative links (`/docs`) resolve on this instance — but on a private instance they need a signed-in browser, and `/llms.txt` is the one page served without a login. The copies mirrored below are what an agent should read. ## Pages on this instance - [Dashboard](/): every site on the instance; signed-in members can drag a folder onto it to deploy - [Docs](/docs): the one-page SDK reference (mirrored below) - [Hosting](/host): running your own instance (mirrored below) - [Changelog](/changelog): every release (mirrored below) - [brisk.js](/brisk.js): the browser SDK, served on every site - [Plugins](/api/plugins): JSON list of the plugins installed here; `/api/plugins/` is a plugin's manifest (a signed-in member's call wherever the instance has a login) ## Elsewhere - [Source](https://github.com/tomperi/brisk): README, architecture, worker internals - [@usebrisk/cli on npm](https://www.npmjs.com/package/@usebrisk/cli): the `brisk` command - [Realtime wire protocol](https://github.com/tomperi/brisk/blob/main/docs/realtime-protocol.md): what `brisk.js` and the worker say to each other - [The Brisk skill](https://github.com/tomperi/brisk/tree/main/skills/brisk): for Claude Code and other coding agents — install instructions next ## For coding agents: install the Brisk skill The repository ships a companion skill, `skills/brisk/`, that teaches a coding agent the lifecycle around Brisk — target an instance, `brisk init`, build with the SDK, `brisk deploy`, verify, iterate, act on teammates' comments — and the product constraints that keep a site a Brisk site. It is self-contained: two markdown files that link only to the public repository and `brisk --help`. **Claude Code** discovers skills in `~/.claude/skills/` (or a project's `.claude/skills/`) from the frontmatter in `SKILL.md`. Install it without cloning: ```sh mkdir -p ~/.claude/skills/brisk/references curl -fsSL https://raw.githubusercontent.com/tomperi/brisk/main/skills/brisk/SKILL.md -o ~/.claude/skills/brisk/SKILL.md curl -fsSL https://raw.githubusercontent.com/tomperi/brisk/main/skills/brisk/references/sdk.md -o ~/.claude/skills/brisk/references/sdk.md ``` From a checkout, `cp -r skills/brisk ~/.claude/skills/brisk` does the same. **Codex and other agents** do not read the frontmatter, so point them at the file directly: reference `SKILL.md` from the project's `AGENTS.md` ("For Brisk work, follow `./brisk/SKILL.md`"), or drop it where the agent already reads project docs. Same content, no second copy to maintain. Then install the CLI (`npm install -g @usebrisk/cli`) and run `brisk login ` once. `brisk init` also drops an `AGENTS.md` with the per-site SDK reference into every folder it scaffolds. The skill follows, then the SDK cheat-sheet it loads on demand, then the pages of this instance. --- ## Building and deploying Brisk apps Brisk is internal hosting: drop a folder of static files, get a live URL, plus six zero-config browser APIs. Your job is to build a small site and ship it with the `brisk` CLI. Repo for deep dives: https://github.com/tomperi/brisk ### The constraints are the product — do not violate them - **Six primitives, no more:** database, identity, AI, files, channels, hosting. If a request seems to need a seventh, it almost always composes from these. - **No custom backend.** Everything is client-side static HTML/CSS/JS. There is no server you write, no API routes, no Node process. State lives in `brisk.db`. - **No build step, no config, no framework required.** A folder, an `index.html`, and ``. Don't add bundlers, package.json, or a dev server unless the user explicitly wants one for authoring ergonomics — what deploys is plain files. - **No permissions / no owners.** Everything is open to every authenticated teammate. Don't build auth rules, roles, or per-user access control. If you catch yourself adding a backend, a config knob, or a seventh primitive, stop — show how the existing pieces cover it instead. ### When to use Brisk Good fit: internal tools, dashboards, guestbooks, voting/poll sites, multiplayer toys, anything small that wants a database / identity / realtime without setup. Not a fit: anything needing a real backend, custom auth/permissions, scheduled jobs (cron), or a public site exposed to untrusted users. ### The standard flow #### 1. Get the CLI and target an instance ```sh npm install -g @usebrisk/cli # or: node path/to/brisk/cli/dist/cli.js brisk whoami # confirm which instance + who you are ``` Brisk runs against an _instance_ (a deployed Brisk server, or local dev). Resolve it before deploying: - **A real instance:** `brisk login brisk.example.com` once — it opens the browser, finishes login, stores a profile. Deploys are attributed to you. - **CI / non-interactive:** set `BRISK_SERVER` and `BRISK_TOKEN`. - **Local dev:** default is `http://localhost:8787` (run `wrangler dev` in the Brisk repo's `worker/`). If it's ambiguous which instance the user means, ask before deploying. #### 2. Scaffold ```sh brisk init my-site # creates brisk.json, index.html, AGENTS.md ``` `brisk init` drops an `AGENTS.md` into the folder with the full per-site SDK reference — read it once you're in the folder. #### 3. Build the page Plain HTML/CSS/JS. Load the SDK and use it directly — no keys, no imports: ```html ``` For the full set of calls (db, me, ai, fs, channels), see [references/sdk.md](#brisk-sdk-cheat-sheet). One folder, `index.html` is the entry. #### 4. Ship ```sh brisk dev # redeploy on every save — use while iterating brisk deploy # ship once ``` #### 5. Verify — always ```sh brisk open # open the deployed URL ``` Load the URL and confirm the page renders and a `brisk.*` call actually works. Never report "done" without seeing it live. #### 6. Remix and inspect existing sites ```sh brisk list # everything deployed on the instance brisk pull dir # download a site's source to remix it ``` ### Plugins Plugins are first-party features a site switches on, injected into its pages by the platform: a curated layer sites _consume_, not a seventh primitive. You can't upload one — the set is fixed per instance (a fork adds them server-side). Each is `mandatory`, on by `default`, or `optional`, and a site opts in or out in `brisk.json`: ```json { "name": "my-site", "plugins": { "comments": false } } ``` **Discover what the instance has — never assume.** The registry differs between instances and forks, and the CLI knows nothing about any specific plugin: it reads the list and each plugin's verbs live from the server. ```sh brisk plugin list # every plugin on this instance: id, requirement, description brisk plugin --help # one plugin's CLI verbs and their arguments brisk deploy # prints which plugins are enabled on the site it shipped ``` The same data is JSON on the instance: `GET /api/plugins` is the list, `GET /api/plugins/` a plugin's manifest (its actions, their args, how to render results), and every verb is `POST /api/plugins//actions/` with `{ "args": { "site": "…", … } }`. Widget-only plugins have no verbs. Plugin data lives in reserved `_plugin:*` collections: a page may read and subscribe to them like any collection, but writes go only through the plugin's actions — direct writes are refused so authorship and the audit trail stay honest. **Comments** (on by default) closes the feedback loop with you: teammates click any element on the deployed site and leave a note. Pull the open feedback, act on it, and report back: ```sh brisk plugin comments export my-site # open feedback as markdown — read this first brisk plugin comments list my-site # the same, as a table brisk plugin comments reply my-site "fixed — sentence-cased the heading" brisk plugin comments resolve my-site ``` Only published comments reach the server; a teammate's unpublished drafts stay in their browser, so an empty export means nothing was published yet. ### Escape hatches (when this skill isn't enough) - `brisk --help` — the full CLI command + option list. - The folder's generated `AGENTS.md` — full per-site SDK reference. - `/docs` on any instance — the live one-page SDK reference. - `/llms.txt` on any instance — everything as one text file: this skill, the docs and hosting pages, the architecture, and the changelog. - https://github.com/tomperi/brisk — README + architecture; websearch it for deeper questions (wire protocol, worker internals, deploying the platform). --- ## Brisk SDK cheat-sheet Load once per page: ``. Then the `brisk` global is available — no keys, no imports, no async setup. All calls are namespaced to the current site. This is the basics; the folder's `AGENTS.md` and `/docs` have the complete reference. ### Database — schemaless JSON collections ```js const posts = brisk.db.collection('posts'); const doc = await posts.create({ title: 'Hello', votes: 0 }); // → {id, createdAt, updatedAt, ...} const all = await posts.list({ sort: '-created', limit: 50 }); // omit opts for oldest-first const one = await posts.get(doc.id); await posts.update(doc.id, { votes: 1 }); // shallow merge await posts.delete(doc.id); const stop = posts.subscribe({ // realtime; returns unsubscribe fn onCreate: (doc) => {}, onUpdate: (doc) => {}, onDelete: (id) => {}, }); ``` ### Identity — who's looking at the page ```js const user = await brisk.me(); // { email, name, picture? } ``` ### AI — server holds the keys ```js const r = await brisk.ai.chat('Summarize this: ...'); // → { text, model, provider } const r2 = await brisk.ai.chat([{ role: 'user', content: 'Hi' }], { system: '...', model: '...', maxTokens: 1024, }); ``` ### Files — permanent URLs for uploads ```js const [file] = await brisk.fs.upload(input.files); // File | File[] | FileList → [{ url, name, size, type }] ``` ### Channels — realtime messaging + presence (multiplayer) ```js const room = brisk.channel('lobby'); room.send({ x: 1 }); // to everyone else in the channel room.on('message', (data, from) => {}); // from = { email, name, picture? } room.on('presence', (members) => {}); // fires on join/leave; members = User[] room.members; // current members room.leave(); ``` ### A complete minimal page ```html guestbook

Guestbook

    ``` --- ## Everything Brisk does One page, because that's all it needs. A Brisk site is a folder of static files plus, if you want it, six backend primitives reachable from one script tag. There are no other features. ### Deploying Install the CLI once, then any folder is one command away from being a site: ``` npm install -g @usebrisk/cli brisk login this-host # sign in once, stores a profile (like AWS profiles) brisk init my-site # scaffold (optional — any folder works) brisk deploy my-site # → https://my-site.your-host/ brisk dev my-site # redeploy on every save brisk list # everything on this instance brisk pull some-site # download any site's source to remix it ``` Signed-in members can also skip the CLI entirely: drag a folder anywhere onto the dashboard, name it, launch. Deploys are atomic: files upload to a fresh version, then the live pointer swaps. Site names are first come, never owned. Anyone signed in can overwrite anything, including this dashboard (deploy a site called `home`). Every publish is a versioned snapshot. Only the current version is ever served; by default the previous one is cleaned up once the pointer swaps, so storage stays bounded. The person running the instance can set `DEPLOY_HISTORY=on` to retain every version instead — groundwork for rollback, which isn't exposed yet. `brisk login` opens the browser, finishes this instance's login, and stores a personal token under a profile — so deploys are attributed to _you_, and you can hold profiles for several Brisk instances at once (`--profile` or `BRISK_PROFILE` picks one; `brisk whoami` tells you where you are). CI skips profiles: `BRISK_SERVER` + `BRISK_TOKEN`. ### The SDK Every site, deployed or not, can load the SDK from its own origin: ``` ``` That's the entire setup. No API keys (they live on the server), no configuration, no build step. `brisk` is a global with five namespaces, each scoped to the site that loads it. One caveat: every namespace below needs the viewer to be _signed in_. On a public (view-only) instance, signed-out visitors can see pages but every `brisk.*` call rejects with a 401 — build demos so they degrade gracefully. ### Database Schemaless JSON collections, Firebase-style. Treat it like a big persisted JSON store: no schemas, no migrations, shape your data freely. ``` const posts = brisk.db.collection('posts'); const doc = await posts.create({ title: 'Hello', votes: 0 }); const all = await posts.list({ sort: '-created', limit: 50 }); await posts.update(doc.id, { votes: doc.votes + 1 }); // shallow merge await posts.delete(doc.id); // realtime: fires for every change anyone makes const stop = posts.subscribe({ onCreate: (doc) => {}, onUpdate: (doc) => {}, onDelete: (id) => {}, }); ``` Docs carry `id`, `createdAt`, `updatedAt`. Collections are namespaced per site, so name things whatever you like — except the reserved `_plugin:*` namespace: plugin data reads (and subscribes) like any collection, but writing it goes through the plugin's own [actions](#plugins), which stamp the author and keep the audit trail honest. ### Identity The platform authenticates members before any page loads, so sites get identity for free. No login flows, ever. ``` const user = await brisk.me(); // { email: 'sam@yourco.com', name: 'Sam', picture: '…' } // rejects with a 401 for signed-out visitors on a public instance ``` ### AI LLM calls straight from the browser. The server holds the provider keys (Anthropic or OpenAI) and proxies the request. ``` const res = await brisk.ai.chat('Turn these notes into a haiku: …'); res.text; // or full control: await brisk.ai.chat( [{ role: 'user', content: 'hello' }], { system: 'You are terse.', model: 'claude-opus-4-8' }, ); ``` ### Files ``` const [file] = await brisk.fs.upload(input.files); file.url; // permanent, immutable URL ``` ### Channels Realtime messaging with presence. This is the multiplayer primitive: cursors, games, live polls, collaborative anything. ``` const room = brisk.channel('lobby'); room.send({ x: 128, y: 64 }); // to everyone else room.on('message', (data, from) => { … }); // from = { email, name } room.on('presence', (members) => { … }); // join/leave room.members; // current list room.leave(); ``` ### Plugins Curated, first-party features a site switches on in `brisk.json`, injected into its pages automatically. A platform layer, not a seventh primitive — sites consume plugins; extending the set is a fork concern. Each is `mandatory`, on by `default`, or `optional`: ``` { "name": "my-site", "plugins": { "comments": false } } ``` **Comments** (on by default) lets teammates leave feedback on any element. Notes start as local drafts (copy them as markdown for an agent — drafts sit in the browser's localStorage, which path-mode sites share, so local ≠ private); publish to share with realtime + a best-effort audit trail. Drive it from the terminal — the same verbs an agent uses: ``` brisk plugin comments list my-site # open feedback, as a table brisk plugin comments export my-site # markdown an agent can act on brisk plugin comments reply my-site "fixed it" brisk plugin comments resolve my-site ``` The widget minimizes to a corner bubble you click to reopen; `Shift+C` fully hides it. `brisk plugin list` shows what's installed; `brisk plugin --help` lists a plugin's verbs, loaded live from the server. ### Philosophy Brisk stays useful by staying small. No site owners, no permissions, no custom backends, no cron jobs. Everything is open to every teammate, which is exactly why a leaderboard takes five minutes instead of a security review. When a feature request shows up, the answer is usually a demonstration that the six primitives above already cover it. Running your own instance — setup, login, subdomains, and cost — is on the [hosting](/host) page; the architecture and internals live in the [repo](https://github.com/tomperi/brisk). --- ## Hosting Brisk Brisk is one Cloudflare Worker plus R2, D1, and Durable Objects — or the same app self-hosted on any Node box with SQLite and a disk. Stand up your own instance, point it at a domain, and decide who gets in. This is the operator's page — building sites on an instance is in the [docs](/docs). ### How it works Every request to every site hits a single Cloudflare Worker. It reads the site from the subdomain (or a `/s//` path), then either streams that site's static files straight from R2, or — for `brisk.*` calls under `/api/` — talks to the backing primitives: D1 for the database, R2 for uploads, a Durable Object per site for realtime, and a server-side key for AI. An optional Google OAuth gate sits in front; with it off, every visitor is a trusted dev user. ![Brisk architecture: site subdomains pass through optional Google OAuth into one Cloudflare Worker, which serves static files from R2 and routes /api/* to D1, R2, Durable Objects, the AI proxy, identity, and hosting.](/architecture.png) ### Stand it up From the [repo](https://github.com/tomperi/brisk): create the resources, apply the schema, ship the Worker. ``` cd worker export CLOUDFLARE_ACCOUNT_ID=... # or run `wrangler login` (single account) npx wrangler d1 create brisk # paste the id into wrangler.jsonc npx wrangler r2 bucket create brisk npx wrangler d1 migrations apply brisk --remote pnpm --filter @usebrisk/sdk build # bundle the SDK into the worker assets npx wrangler deploy ``` That alone gives path-mode URLs (`https://brisk..workers.dev/s/foo/`) with no auth — fine on a private network. The full experience adds wildcard subdomains and a login. Instance config (`BASE_HOST`, `AUTH`, the allowlists, `VISIBILITY`) isn't committed to `wrangler.jsonc` — it's deployment-specific. Set it as **Variables** in the dashboard (Workers & Pages → your worker → Settings → Variables and Secrets); every var is optional and defaults to a path-only, no-auth, private instance. Locally, `wrangler dev` reads them from `.dev.vars`. ### Self-hosting (Docker, Compose, Kubernetes) Cloudflare is the reference target, not a requirement. The same core runs as a plain Node server with the platform's seams bound to SQLite, the filesystem or any S3-compatible bucket, and in-process websockets — the SDK, the dashboard, and every deployed site behave identically. The fastest way in is the wizard, which asks a few questions and writes the config for whichever target you pick. It lives in this repo, so run it from a checkout: ``` pnpm install && pnpm build node create-brisk/dist/index.js ``` Or go straight to the artifacts in [deploy/](https://github.com/tomperi/brisk/tree/main/deploy): ``` # a single VM cp deploy/.env.example deploy/.env # set AUTH, BASE_HOST, secrets… docker compose -f deploy/docker-compose.yml up -d # Kubernetes helm install brisk deploy/helm/brisk \ --set config.baseHost=brisk.example.com \ --set config.auth=google \ --set secrets.googleClientId=… \ --set secrets.googleClientSecret=… \ --set secrets.sessionSecret=$(openssl rand -hex 32) \ --set ingress.enabled=true ``` Storage defaults to filesystem-plus-SQLite on one volume — no external services; set `config.storage=s3` — plus the `s3.*` endpoint and bucket values and the S3 credentials under `secrets` — to put objects in any S3-compatible bucket instead. Realtime lives in-process, so run exactly one replica (the Helm chart refuses to render more). The [deploy/README](https://github.com/tomperi/brisk/blob/main/deploy/README.md) holds the full environment-variable reference and the backup story: **the volume is the database** — nothing replicates it for you. The login, demo-mode, and AI sections below apply to a self-hosted instance too — every `wrangler secret put` becomes the same variable in `.env` or the Helm values. Subdomains need a wildcard DNS record and TLS pointed at your box instead of Cloudflare's; demo mode's rate-limiting advice is Cloudflare's, so bring your own (usually at the reverse proxy); the cost section stops applying entirely (the cost is the box). ### Wildcard subdomains Serve `foo.brisk.example.com` by setting the `BASE_HOST` variable to the host your sites hang off of, then attaching the domain and route **in the dashboard** (Workers & Pages → your worker → Settings → Domains & Routes): - custom domain: `brisk.example.com` - route: `*.brisk.example.com/*` (zone: `example.com`) Don't add these as a `routes` key in `wrangler.jsonc`: with routes in the config, `wrangler dev` rewrites every local request's Host to the production zone, so `foo.localhost` breaks and the API hands out production URLs from local dev. Dashboard-attached domains persist across deploys, so it's one-time setup. You also need a wildcard DNS record (`*.brisk` → CNAME to the apex). The matching TLS certificate is the one part that can cost money — see [Cost](#cost) for the free path (keep sites one level deep, or use a dedicated domain). ### Google login Optional single sign-on for the whole instance: Google OAuth on the apex, with the session cookie scoped to the parent domain so one login covers every site subdomain. 1. Create an OAuth client (web application) in Google Cloud Console with redirect URI `https://brisk.example.com/auth/callback`. 2. Set the secrets: ``` npx wrangler secret put GOOGLE_CLIENT_ID npx wrangler secret put GOOGLE_CLIENT_SECRET npx wrangler secret put SESSION_SECRET # any long random string npx wrangler secret put DEPLOY_TOKEN # optional, for CI ``` 3. Set the auth variables in the dashboard: `AUTH=google`, and restrict who gets in with `ALLOWED_EMAIL_DOMAINS=yourco.com` for a company, or `ALLOWED_EMAILS=you@gmail.com` for a personal instance (never allowlist all of `gmail.com`). Either list admits; both empty admits anyone with a Google account. Browsers get bounced to Google; the CLI logs in as a real person with `brisk login brisk.example.com`, and deploys are attributed to that email. The `DEPLOY_TOKEN` secret is the CI credential (`BRISK_TOKEN`, shows up as `ci@brisk`). The default `"AUTH": "none"` skips all of this and treats every visitor as a trusted dev user — only do that on a network you trust. ### Public demo mode Instances are private by default — everything but `/llms.txt` needs a login. To let strangers _see_ your sites without being able to touch them, set `VISIBILITY=public` alongside `AUTH=google`. - **Visitors (no login)** can browse the dashboard, the docs, and every deployed site. Their static requests are edge-cached (`max-age=300`), so a busy demo costs almost no R2/D1 operations — the trade-off is a deploy can take up to five minutes to reach signed-out eyes. - **Everything else is a 401 for visitors** — the database (reads included), AI, uploads, websockets, deploys, and CLI token minting. Demo pages render; their `brisk.*` calls fail fast, so build demos to degrade gracefully. - **Members** (whoever passes your allowlist) sign in and get the full, uncached platform. The API has no rate limiting, so before publishing spend two minutes in the Cloudflare dashboard on your zone — both are free and stop abusive traffic before it bills as Worker requests: - **Security → WAF → Rate limiting**: one rule on your host, e.g. block for 10 s when an IP exceeds ~100 requests. - **Security → Bots**: enable Bot Fight Mode. And leave `DEPLOY_TOKEN` unset unless you actually have CI — an unset secret means that auth path doesn't exist at all. ### AI keys Set the provider keys as secrets and `brisk.ai` works on every site with no per-site setup — keys stay on the server. ``` npx wrangler secret put ANTHROPIC_API_KEY # and/or OPENAI_API_KEY ``` Usage bills against your own provider account at their rates — see [Cost](#cost). ### Cost Brisk is built to run inside Cloudflare's free tier. At personal or small-team scale the whole platform — hosting, database, file storage, and realtime — fits inside it, so it costs nothing. Here's what each piece uses and the free allowance it draws from (per Cloudflare account): | Product | Brisk uses it for | Free tier | | --- | --- | --- | | Workers | every request to every site and API | 100k requests/day, 10 ms CPU/request | | R2 | site files + `brisk.fs` uploads | 10 GB stored, 1M writes + 10M reads/mo, zero egress | | D1 | the site registry + `brisk.db` documents | 5 GB, 5M row-reads/day, 100k row-writes/day | | Durable Objects | one realtime room per site (`brisk.channel`, db events, presence) | included on the free plan (SQLite-backed) | **Realtime stays free even idle.** Each site's room is a SQLite-backed Durable Object using WebSocket hibernation, so empty rooms bill nothing — you only pay while messages actually flow. And on a public instance, signed-out static views are edge-cached (`max-age=300`), so demo traffic mostly never reaches R2 or D1 at all. **When you'd cross into paid.** 100k requests/day is a lot of headroom for an internal instance. Exceed it and you're on [Workers Paid](https://developers.cloudflare.com/workers/platform/pricing/) ($5/mo), which turns the daily caps into a forgiving monthly pool (10M requests + 30M CPU-ms included, cheap overage after). At that point R2, D1, and Durable Object usage beyond the included amounts also start metering, but for most instances the $5 base is the whole story. **The one catch is wildcard TLS.** Cloudflare's free Universal SSL covers a domain's apex and _one_ level of subdomain. Sites one level deep — `foo.example.com`, with `BASE_HOST=example.com` — are covered for free. Nest them under a label — `foo.brisk.example.com` — and that second-level wildcard needs an [advanced certificate](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/) (~$10/mo). Two ways around it: register a dedicated domain (~$10/_year_ at Cloudflare Registrar, at cost) and point `BASE_HOST` at its apex so every `*.yourdomain` site gets free TLS; or skip subdomains entirely and serve path-mode URLs (`/s/foo/`), which never need a wildcard. **AI is pass-through.** `brisk.ai` calls bill against the server's own Anthropic or OpenAI key at provider rates — the only cost here with no ceiling, so set a spend limit on the provider side if your sites lean on it. Bottom line for a personal instance: **$0/mo** on path mode with no AI, or about **$10/year** (a dedicated domain) for clean subdomain URLs with free TLS — plus whatever AI tokens your sites spend. --- ## Architecture The whole platform is one app behind six seams. On Cloudflare those seams are a Worker and four Cloudflare primitives: ``` foo.brisk.example.com ─┐ bar.brisk.example.com ─┤→ Worker ──→ /s/… static files ──→ R2 (versioned deploys) brisk.example.com ─────┘ │ (Google OAuth here, ├→ /api/db, /api/sites ──────→ D1 (docs + deploy pointers) cookie covers *.domain) ├→ /api/ws ──────────────────→ Durable Object per site ├→ /api/ai ──────────────────→ Anthropic / OpenAI (server keys) └→ /api/fs, /files ──────────→ R2 (uploads) ``` - **Deploys are atomic**: files upload under a fresh version prefix in R2, then the site's pointer row in D1 swaps. A site is never served half-updated. By default the previous version is deleted after the swap, keeping storage bounded; set `DEPLOY_HISTORY=on` to retain every version for history and future rollback. - **Realtime is one Durable Object per site** (websocket hibernation, so idle rooms cost nothing). It fans out db change events, channel messages, and presence. - **Identity is platform-level**: Google OAuth on the apex, JWT session cookie scoped to the parent domain, every request arrives pre-authenticated — the same trick as putting a VM behind an identity-aware proxy. - **The runtime is a seam, not an assumption.** Storage, database, rooms, assets, cache, and background work each sit behind an interface ([`worker/src/platform`](https://github.com/tomperi/brisk/blob/main/worker/src/platform)); the core imports the interface, never a vendor. The Cloudflare assembly binds them to R2, D1, and Durable Objects; the Node assembly binds them to S3-or-disk, SQLite, and in-process websockets. The same integration suite runs against both, so "works on Cloudflare" and "works self-hosted" are the same claim. ### Request flow 1. A request arrives; the Worker derives the **site** from the subdomain, the `/s//` path prefix, or the SDK's `x-brisk-site` header. The bare host is just a site named `home` (the built-in dashboard, until someone deploys over it). 2. The **auth middleware** resolves a user: dev identity (`AUTH=none`), session cookie, or CLI bearer token. Unauthenticated browsers bounce to Google; APIs get a 401. 3. Static requests look up the site's live deploy pointer (cached ~5s per isolate) and stream the file from R2, resolving `/about` → `about.html` and directory indexes. A site whose deploy is a single HTML page — one dropped file — serves that page at `/` too, since there's nothing else it could be. API requests hit D1/R2 directly; `/api/ws` upgrades are handed to the site's Durable Object with the user attached. ### Storage layout | Where | What | | --------------------------------- | --------------------------------------------------------------------------------------------------------------- | | R2 `deploys///…` | site files; one immutable prefix per deploy | | R2 `uploads///` | `brisk.fs` uploads, immutable URLs | | D1 `sites` | one row per site: live-deploy pointer, size, who deployed last | | D1 `deploys` | one row per published version: version number, size, who; pruned to the live version unless `DEPLOY_HISTORY=on` | | D1 `docs` | the document store: `(site, collection, id) → JSON` | | Durable Object `SiteRoom()` | websocket fan-out: db events, channel messages, presence | Nothing else persists. Deleting a site removes its row, docs, deploys, and uploads. --- ## Changelog All notable changes to Brisk are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Tagged releases are published to [GitHub Releases](https://github.com/tomperi/brisk/releases). ### [Unreleased] #### Added - **`/llms.txt`** — everything about Brisk as one text file for coding agents: the companion skill and how to install it, the `/docs` and `/host` pages, the architecture, and the changelog, generated from the repo at build time so it always matches the running version. It is the one page every instance serves without a login, private ones included, since the agents it is for can't complete OAuth — apex only, and it holds nothing but the repo's docs. - **Skill** — the Brisk skill explains plugins and how an agent discovers the ones an instance has (`brisk plugin list`, `/api/plugins`) instead of assuming a registry, and how to act on teammates' comments. ### [0.4.2] - 2026-08-30 #### Fixed - **Comments** — a pin now hides while the element it marks is hidden. Stacked slides, tabs and accordions keep laying their hidden panels out on screen, so every comment on the page used to pin itself over whatever you were actually looking at. Pins follow the page as it swaps panels, with no reload. #### Changed - **Comments** — pins number per author: yours count 1, 2, 3 whatever anyone else leaves alongside them, and a draft keeps its number when you publish it. The pin's tooltip and the panel row name whose a comment is. ### [0.4.1] - 2026-08-11 #### Added - **Deploy a single HTML file.** `brisk deploy report.html` (and `brisk dev`) publishes a one-page site named after the file, and dropping one file on the dashboard names the site the same way. A deploy whose only top-level page is one HTML file serves it at `/` as well as under its own name, instead of landing on "there's no page at /". Two or more top-level pages stay a 404 — which one is home would be a guess. Deploying a single file that isn't a page links to the file itself and says so, rather than pointing at a root that has nothing to serve. #### Fixed - `.htm` pages are served as `text/html` instead of `application/octet-stream`, so browsers render them (and plugin widgets load on them) rather than downloading the file. - Dismissing the dashboard's deploy overlay clears the pending overwrite, so a later drop can no longer replace a site someone else owns without the "belongs to …" confirmation. - Site URLs (the link the CLI prints after a deploy, the dashboard's live link, and `/api/sites`) are `https://` on an instance behind a TLS-terminating proxy, instead of the plain-http scheme the request reached the server with. `X-Forwarded-Proto` decides it when a proxy sends one, but only when it carries `http` or `https`, and the visitor cache varies by the result — so no single visitor's header can rewrite the links everyone else is handed. The guess is made against the host each URL actually names, so a proxy that rewrites `Host` to a backend address no longer produces an `http://` OAuth `redirect_uri` for an https instance. Where a public certificate can't reach — loopback, bare IPs, and the private-use TLDs `.internal`, `.local`, `.home.arpa` — links stay on http. `deploy/README.md` documents the order and the one case that still needs `X-Forwarded-Proto`. ### [0.4.0] - 2026-07-29 #### Added - **Plugins** — a curated, forkable layer of first-party features a site opts into (`brisk.json` `"plugins": { … }`), injected into served pages and driven from a generic `brisk plugin` CLI whose verbs load from the server. Adding a plugin in a fork is one directory under `worker/src/plugins/` plus a registry line — no worker-core or CLI changes. - **Comments plugin** (on by default) — leave feedback on any element, marked by numbered pins (dashed = draft, blue = open, gray = resolved). Drafts are local-first (localStorage; editable, copy as markdown to hand an agent), publish to share via `brisk.db` with realtime + an audit trail. Resolve, reply, and soft-delete from the widget or the CLI (`brisk plugin comments …`), or copy the whole log as markdown. Right-click the toolbar for the command menu. Typing in the widget never triggers the host page's own shortcuts. Minimize it to a corner bubble (click to reopen); `Shift+C` fully hides it. #### Changed - The `/host` page now covers the self-hosted path (Docker Compose, Kubernetes/Helm, `npm create brisk`, storage options) alongside Cloudflare, instead of documenting the Worker deploy only. #### Fixed - The `/changelog` page renders `**bold**` in entries instead of showing the literal asterisks. ### [0.3.0] - 2026-07-13 #### Added - A provider-agnostic platform abstraction (storage, database, rooms, assets, cache) behind six seams, so Brisk now runs on Node/self-host — with SQLite plus S3 or the filesystem — in addition to Cloudflare, from the same core. - Self-host packaging under `deploy/`: a multi-stage Dockerfile that builds and runs the Node assembly, a Docker Compose stack (filesystem + SQLite by default, optional MinIO for S3), and a Helm chart for Kubernetes. - `npm create brisk` — an interactive wizard that scaffolds a self-host deployment. Pick a target (Docker Compose, Kubernetes/Helm, or Cloudflare Workers), auth mode, and storage backend, and it writes the matching deployment config (`docker-compose.yml` + `.env`, `brisk-values.yaml`, or `.dev.vars` + a Cloudflare checklist) plus next steps. - The dashboard's drag-and-drop deploy takes a "deploying as" name, so a browser deploy records an `owner` the way `brisk deploy --username` already does. The name is remembered between drops. Dropping onto a site someone else owns now surfaces the same 409 the CLI does and asks you to launch again to overwrite, instead of failing with no way through. #### Fixed - Google sign-in works behind a TLS-terminating reverse proxy. The OAuth `redirect_uri` (and login URLs and the session cookie's `Secure` flag) were derived from the request the app received — plain `http` when a proxy terminates TLS and doesn't forward the scheme — which Google rejects with `redirect_uri_mismatch`. `X-Forwarded-Proto` is now honored when present, and any non-localhost host otherwise resolves as `https`. - Deploys are attributed to the asserted deployer, so the dashboard's "by" column matches the `owner` label instead of the authenticated account. On `AUTH=none` every request is the same `Dev` user, which made every deploy read as `Dev` no matter who shipped it. - The 404 page tells a missing path apart from a missing site. A typo'd path on a live site used to answer with `brisk deploy --site ` — instructions that would have overwritten the very site you were browsing. It now links back to the site's root, and only an unclaimed name gets the deploy hint. ### [0.2.0] - 2026-07-06 #### Added - Deploys record a self-asserted `owner` (from `--username`, else the profile). Overwriting a site owned by someone else now needs confirmation — the CLI prompts, or pass `--force` / `BRISK_FORCE=1`. A spoofable label and footgun guard, never access control: unowned sites and every other action stay open. - `DEPLOY_HISTORY=on` retains every published deploy as an immutable version in a new `deploys` table — version history now, rollback later. Off by default: the previous version is deleted after each atomic swap, keeping storage bounded. - A `/changelog` page rendered from this file and linked from the dashboard nav; tag pushes now cut their GitHub release notes from the same `CHANGELOG.md`. - OpenGraph image and social meta tags, so shared Brisk links unfurl with a preview card. - A `room` example — a Three.js isometric diorama. #### Changed - Expanded the self-hosting guide with a how-it-works walkthrough, public demo mode, and an architecture diagram. - Bumped `wrangler` and `esbuild`, with a pinned `undici` override. #### Fixed - Security hardening: `/auth/cli` browser sessions must confirm through a CSRF-guarded consent page before a token is minted, API and dashboard responses carry `nosniff` and anti-framing headers, and uploads serve as attachments. - Closed a visitor cache-poisoning hole — static pages resolve the site from the host only, never a client header — and an unset `AUTH` on a public host now fails closed instead of serving an open backend. - Hardened the open-instance guard so only a literal `AUTH=none` opens a public host: `AUTH` is trimmed, and any other non-`google` value — a mis-cased `Google` or a misspelled `googl` — now fails closed with the same 503 as an unset `AUTH`, instead of silently serving an anonymously-writable backend. - An open (`AUTH=none`) public instance can no longer ship silently: the fail-closed 503 now spells out the secure setup, the worker warns once when open on purpose, and `brisk deploy` warns and confirms before pushing to an open host (bypass with `--yes` / `BRISK_YES=1`). - Generated assets (`/brisk.js` and the `/changelog` page) are now built during `wrangler deploy`, so they ship with the worker instead of 404ing. - Dashboard mobile layout: compact site rows, tighter headings, and a header that wraps instead of breaking mid-word. ### [0.1.0] - 2026-06-14 #### Added - Folder-to-site deploys on a single Cloudflare Worker, backed by R2, D1, and Durable Objects — atomic version swaps, first-come site names, no config files. - The `brisk` CLI: `init`, `deploy`, `dev` (watch and redeploy on every save), `list`, `open`, and `pull` to download any site's source. `brisk login` stores AWS-style per-instance profiles (`whoami`, `profiles`, `profile use`). - The zero-dependency browser SDK served at `/brisk.js`: `brisk.db` (schemaless collections with realtime subscriptions), `brisk.fs` (uploads to permanent URLs), `brisk.channel` (realtime messaging and presence), `brisk.me` (identity), and `brisk.ai` (LLM calls proxied through the server's Anthropic/OpenAI keys). - Realtime over one Durable Object per site — database change events, channel messages, and presence — using WebSocket hibernation so idle rooms cost nothing. - Optional Google OAuth on the apex domain with a session cookie scoped to every site subdomain, plus `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` allowlists, personal CLI tokens, and a CI deploy token. - `VISIBILITY=public` demo mode: signed-out visitors get edge-cached, view-only access while every `brisk.*` API stays members-only. - The dashboard at the apex domain — a live list of every site, drag-and-drop deploys with a launch flow and confetti, a one-page SDK reference at `/docs`, and a self-hosting guide at `/host`. - `@usebrisk/cli` and `@usebrisk/sdk` published to npm, cut in lockstep by a tag-driven release workflow. [unreleased]: https://github.com/tomperi/brisk/compare/v0.4.2...HEAD [0.4.2]: https://github.com/tomperi/brisk/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/tomperi/brisk/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/tomperi/brisk/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/tomperi/brisk/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/tomperi/brisk/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/tomperi/brisk/releases/tag/v0.1.0