A tiny self-hosted server for storing and browsing self-contained HTML "plans". AI agents push plans as HTML via an authenticated REST API; humans browse them in a React UI. Designed to run on a headless box reachable over a Tailnet.
Each plan is a single .html file stored on disk, plus an index.json catalog.
The index is rebuilt from the .html files on startup, so deleting index.json
is non-destructive.
- Bun (runtime + test runner)
cp .env.example .env # then edit .env and set VISUALPLANNER_TOKEN
bun install
bun run dev # API server on :7070 (watches for changes)
bun run dev:ui # Vite UI on :5173 (proxies /api -> :7070)Open http://localhost:5173 in development.
For production (builds the UI, then serves it from the API origin on :7070):
bun run start| Script | Description |
|---|---|
bun run dev |
Run the API server with --watch (port 7070). |
bun run dev:ui |
Run the Vite dev server for the UI (port 5173, proxies /api). |
bun run build |
Build the UI into dist/client/. |
bun run start |
Build, then run the production server (serves UI + API on :7070). |
bun run test |
Run the test suite (bun:test). |
bun run typecheck |
Type-check with tsc --noEmit. |
bun run lint |
Lint with Biome. |
bun run format |
Format the codebase in place with Biome. |
bun run check |
Apply Biome's safe lint + format fixes in place. |
All settings are optional environment variables (read from .env in the working
directory, with real process.env taking precedence):
| Variable | Default | Meaning |
|---|---|---|
VISUALPLANNER_TOKEN |
change-me (disabled) |
Bearer token agents must present to write. Unset/change-me = no auth. |
VISUALPLANNER_PORT |
7070 |
Port to listen on. |
VISUALPLANNER_DATA_DIR |
~/.local/share/visualplanner |
Where plan .html files and index.json are stored. ~ is expanded. |
VISUALPLANNER_BIND |
auto (Tailscale IP, else 127.0.0.1) |
Hostname to bind. Empty/unset auto-detects a Tailscale 100.x address. |
- Writes (
POST/PUT/DELETE) require the token, sent as eitherAuthorization: Bearer <token>orX-VisualPlanner-Token: <token>. - Reads (
GET) are always unauthenticated, even when a token is set. - If
VISUALPLANNER_TOKENis unset or literallychange-me, auth is disabled entirely. The server then refuses to bind beyond localhost — if no token is set and the resolved bind address is not loopback, startup aborts with an error. Set a real token to listen on a Tailscale interface. - The API is same-origin only (no CORS headers). The SPA is served from the same origin, so it works unchanged; other websites cannot read plans cross-origin. Reads are still unauthenticated, so anyone who can reach the host directly can read plans — keep sensitive content out of them.
Generate a token with:
openssl rand -hex 32| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/plans |
none | List plan metadata, newest-first. |
GET |
/api/plans/:id |
none | Get one plan's metadata. |
GET |
/api/plans/:id/html |
none | Get a plan's raw HTML (text/html). |
POST |
/api/plans |
token | Create a plan. Returns 201 { plan }. |
PUT |
/api/plans/:id |
token | Update a plan in place (id stable). |
DELETE |
/api/plans/:id |
token | Delete a plan. Returns { ok: true }. |
The plan id is derived as slugify(project)-slugify(title), with a -N
suffix appended on collision (e.g. my-project-refactor-the-foo-2).
curl -X POST http://localhost:7070/api/plans \
-H "Authorization: Bearer $VISUALPLANNER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project\":\"demo\",\"title\":\"hello\",\"html\":\"<h1>Hi</h1>\"}"src/
client/ Vite + React SPA (shadcn/ui)
components/ App components + vendored shadcn ui/ primitives
server/ Hono + Bun API, disk-backed Store, config, auth
shared/ Types shared between client and server
skill/ Agent authoring skill + canonical plan stylesheet (plan.css)
The skill/ directory holds the publishing skill that agents use to generate
plans in the VisualPlanner house style, plus plan.css (the canonical stylesheet)
and gallery.html (an example).
{ "project": "my-project", // default "untitled" "title": "Refactor the foo", // default "Untitled plan" "sessionId": "abc123", // optional "tags": ["refactor", "backend"], // optional "html": "<!doctype html>..." // required, non-empty }