Features
Every capability is always compiled. The headline is the feature; the second column names the technology that powers it. The removal class of whatever sits behind a row (core / plugin / feature) lives in Scope taxonomy — the registry is the source of truth, and a bracketed (plugin) marks a technology that serves a feature rather than being one.
| Feature | Powered by | Runtime opt-out | What it does |
|---|---|---|---|
| Auth | Owned argon2id + sessions | — (middleware stays; login UI removable) | Cookie sessions, demo login, per-request user loading |
| Access control (RBAC) | Casbin + users.role | — (middleware always wired; RBAC_ENABLED=true enforces) | Roles on the user row (admin, everything else) with the policy in one Casbin model, enforced before the handler runs. Embedded policy is the default; RBAC_POLICY_SOURCE=db moves it to casbin_rule. Demo accounts ship as admin/viewer. See RBAC |
| Todo app + realtime | SSE Hub record-sync + ephemeral signals | — | DB actions (create/toggle/delete) stream visibility-scoped via owner + visibility (private rows to owner tabs, shared rows to every login). Ephemeral signals carry toasts, clients count, AI suggest |
| Background jobs + retry | goqite + retry-go | — | Background jobs with backoff/jitter. Stepper UI streamed via SSE (techStep / techPhase) |
| AI Suggest | GoAI | GOAI_API_KEY unset | LLM call from the todo UI; button hidden when no key. Stepper signals aiStep / aiPhase. Proves the queue+retry path against a real provider, including Biller metering when wired |
| Ask AI (generative UI) | Catalog + SSE morphs, no client JS | GOAI_API_KEY unset (SIMULATE_LLM demo answers keyless), or trim genui (bundle with Chat) | Model picks cards, tables, sections from a fixed registry; unknown types fail closed; one bounded repair attempt feeds a rejected answer's validation error back to the model before the failure reaches the user; worker streams the render over a dedicated SSE hub (/genui). Optional JEV_ROUTER_ENABLED=true lets a jev-compatible classifier pick which catalog component the answer leads with (off by default; any failure falls back to the full catalog). Proves catalog-constrained generation (the model cannot emit arbitrary HTML) with unknown-type fail-closed |
| Chat (conversation) | Invented visuals (sanitized) + catalog fallback + persisted history + the app's own panels | GOAI_API_KEY unset (SIMULATE_LLM demo answers keyless), or trim genui (bundle with Ask AI) | Conversation where the answer is invented for the request: the model composes Tailwind/DaisyUI markup (html_block, advertised first in the prompt), sanitized server-side through a closed allowlist, and limited to a safelisted class vocabulary (anything outside it renders unstyled, so the prompt names the exact list), with the catalog kept for what markup cannot do — so chat does not read like Ask; and todo_list {filter} for questions about the user's OWN todos, whose rows the worker hydrates from the store (the model picks the slice, never the content; a panel that cannot be hydrated is dropped, not shown empty); prompts persist per owner and history is server-rendered on load, answers render inline as text plus components patched into the live column; a payload that fails validation degrades to the prose around it, never to raw JSON in the bubble; dedicated SSE hub (/chat). Proves persisted per-owner history with invented-but-safe visuals |
| Review loop | Genkit flows (plugin) + GoAI + jev classifier judge | GOAI_API_KEY unset (real-LLM only; page shows a setup hint), or trim review | Writer → critic (bounded) → classifier judge, streamed verdict, discarded. Zero server-side artifacts (billing ledger only). Any jev-compatible judge via JEV_* env — see Review loop. Proves a typed multi-role loop with a classifier judge, per-role cost attribution, and a confidence cascade to human approval |
| AI credits + BYOK | ai-credits | CREDITS_ENABLED=false | Optional plugin: meter Todo AI Suggest with reserve/settle, expose balances/top-ups, and proxy a user's encrypted provider key through an OpenAI-compatible BYOK relay |
| Collaborative whiteboard | Loro CRDT + Rough.js + NATS | trim whiteboard (bundle with notes + collab transport), or COLLAB_SNAPSHOTS=memory for snapshot-free local mode | Canvas, SSE + NATS broadcast, live in-progress drawing (the shape under the pointer is relayed on the volatile presence channel so peers watch it grow, then committed by the add op on release), offline-first outbox replay, Turso-persisted snapshots, server-stamped cursor identity (roster and cursors kept in separate maps; idle cursors expire on an 8 s TTL) |
| Shared notes | Loro Text (server-owned) + SSE Hub | trim whiteboard (bundle with whiteboard + collab transport) | Plain-text collab notes: character ops merge server-side, resolved text streams to peers, presence + typing + line carets (each caret measured against the reporter's own text snapshot, so a dot never lands on the wrong line while the two editors are briefly diverged), live index via the Hub record event, snapshots in the notes table |
| Durable workflows | DagNats over JetStream | DAGNATS_ENABLED=false | JSON workflows — HTTP API on :8090, durable state on :4222 (e.g. WelcomeOnboarding) |
| Room presence (entity actors) | GoAkt grains (standalone) | GOAKT_ENABLED=false | One grain per room: heartbeat roster + exactly-one presenter lock, supervised with restart budget. Demo at /room/ with a crash hook |
| Multi-instance realtime | NATS JetStream | NATS_ENABLED=false | JetStream fan-out for todo + whiteboard sync across >1 instance behind a LB |
| Hybrid offline sync | Service Worker + NATS Leaf Node + idempotency | OFFLINE_SYNC_ENABLED=false | NATS CRUD proxy + Service Worker offline queue (default on). Desktop edges publish CRUD ops via JetStream; web clients use Service Worker + Background Sync |
| UI skins (pluggable) | DaisyUI v5 + BasecoatUI | UI_SKIN | DaisyUI (default) or BasecoatUI (shadcn-style OKLCH tokens). Switch via env var or ?skin=. See UI skins |
| Persistence | Turso (sqlc) | — | One data layer: internal/db migrations + generated queries, no strategy switch |
| Desktop-edge sync | NATS Leaf Node | NATS_LEAFNODE_URL unset | Leaf-Node JetStream replication of Loro updates for desktop/edge clients |
| Landing page | Templ | — | The app's own public page on GET / (not the GitHub Pages promo site, which is never installed). No auth, no DB. Todo demo lives at /todo |
| Read-only config view | Templ | — | Auth-gated GET /config: env-decrypted values, masked secrets, runtime constants. Never mutates state |
Adding a new feature
- Create
features/<name>/with its HTTP handlers + Templ components. - Wire it in
internal/server/chi.go— build the handler inBootChi, mount the routes inbuildChiMux. - Use goqite for async work, the SSE Hub for user-facing feedback, and Datastar for the reactive frontend.
- Add a
SCOPE:annotation —make check-scopeenforces it. - Add a
Routes()method on your handler and mount it inbuildChiMux.
The Todo feature is the full reference implementation.
The surfaces you get
| Surface | URL | Notes |
|---|---|---|
| Landing page | / | Public. Same page for guests and signed-in users |
| Todo demo | /todo | Auth-gated. Seeded demo account |
| Ask AI demo | /genui | Auth-gated. Keyless demo answers; live model with GOAI_API_KEY |
| Chat demo | /chat | Auth-gated. Persisted per-owner history; model-invented markup (sanitized) with catalog fallback |
| Review loop | /review | Auth-gated. Writer → critic → classifier judge, streamed verdict, discarded. Runs on the genkit plugin |
| Whiteboard | /whiteboard | Auth-gated |
| Room demo | /room | Auth-gated. Heartbeat roster + presenter lock owned by a GoAkt grain |
| Config view | /config | Auth-gated, read-only |
| DagNats console | :8090 / /dagnats/ | Workflow runs and steps |
Details in Admin & Dashboard.
Removing a feature
Every feature package carries a SCOPE:feature annotation. The rule: delete the package directory, delete its dependents, remove the wiring call from internal/server/chi.go, and — if it was a plugin — remove the start* call in cmd/web/main.go. Full checklist in Scope taxonomy.
Client weight
Vendored JS per feature, measured deterministically (wc -c raw, gzip -nc for wire; rounded up). CSS is one shared bundle and is not attributed per feature.
| Feature | Files | Raw | Gzip |
|---|---|---|---|
| Todo, Room, Landing, Config, Ask AI, Chat | inline only (no extra JS) | 0 KB | 0 KB |
| Shared notes | notes.js | 28 KB | 10 KB |
| Whiteboard | whiteboard.js + rough.min.js | 56 KB | 19 KB |
| Shared core (every page) | datastar.js + theme.js | 40 KB | 16 KB |
Whiteboard and notes pages additionally load the shared throttle.js (~2 KB raw, ~1 KB gzip) — leading + trailing throttle for cursor/caret reports, one implementation for both features.
Split rule (Datastar vs vanilla, enforced by review): list fragments and UI state that patch cleanly go through the Datastar runtime (todo, whiteboard list, notes index — every one of those pages loads datastar.js, pinned by test); canvas rendering, textarea math, and op batching stay vanilla JS (no declarative equivalent, and the offline outbox needs exact control). Shared chrome counts as UI state: the navbar logout form carries data-on handlers, so every page rendering the navbar loads the runtime too (board, notes doc, room, config, landing) — a data-on/data-bind attribute on a page without the runtime is dead markup, and here it would silently skip the logout service-worker cleanup.
Session visibility is shared the same way: the notes and whiteboard streams open with a collab.SessionEvent frame, and pages without a raw stream (todo, room) poll /api/session, so every tab raises the same components.SessionBanner the moment a cookie expires — peer names never silently degrade to client hashes with nothing on screen explaining why, and the banner never auto-redirects (unsent work would be stranded). It is opt-in per page via the render-time authed flag, so a public page never nags about a session it never had.
Method: for f in <files>; do wc -c < $f; gzip -nc $f | wc -c; done. datastar.js is 34 KB raw / ~13 KB gzip — that is the "~12 KiB client" in the README (wire size). Icons (iconify, 24 KB raw) load only on pages that use them; sounds (cuelume.js, 10 KB) only with the sounds plugin.
Related
- Seven async layers — the topology behind these capabilities.
- The Todo example — the pattern to imitate.
- Desktop & Mobile — two more frontends over the same backend.