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.

FeaturePowered byRuntime opt-outWhat it does
AuthOwned 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 + realtimeSSE 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 + retrygoqite + retry-go—Background jobs with backoff/jitter. Stepper UI streamed via SSE (techStep / techPhase)
AI SuggestGoAIGOAI_API_KEY unsetLLM 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 JSGOAI_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 panelsGOAI_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 loopGenkit flows (plugin) + GoAI + jev classifier judgeGOAI_API_KEY unset (real-LLM only; page shows a setup hint), or trim reviewWriter → 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 + BYOKai-creditsCREDITS_ENABLED=falseOptional 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 whiteboardLoro CRDT + Rough.js + NATStrim whiteboard (bundle with notes + collab transport), or COLLAB_SNAPSHOTS=memory for snapshot-free local modeCanvas, 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 notesLoro Text (server-owned) + SSE Hubtrim 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 workflowsDagNats over JetStreamDAGNATS_ENABLED=falseJSON workflows — HTTP API on :8090, durable state on :4222 (e.g. WelcomeOnboarding)
Room presence (entity actors)GoAkt grains (standalone)GOAKT_ENABLED=falseOne grain per room: heartbeat roster + exactly-one presenter lock, supervised with restart budget. Demo at /room/ with a crash hook
Multi-instance realtimeNATS JetStreamNATS_ENABLED=falseJetStream fan-out for todo + whiteboard sync across >1 instance behind a LB
Hybrid offline syncService Worker + NATS Leaf Node + idempotencyOFFLINE_SYNC_ENABLED=falseNATS 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 + BasecoatUIUI_SKINDaisyUI (default) or BasecoatUI (shadcn-style OKLCH tokens). Switch via env var or ?skin=. See UI skins
PersistenceTurso (sqlc)—One data layer: internal/db migrations + generated queries, no strategy switch
Desktop-edge syncNATS Leaf NodeNATS_LEAFNODE_URL unsetLeaf-Node JetStream replication of Loro updates for desktop/edge clients
Landing pageTempl—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 viewTempl—Auth-gated GET /config: env-decrypted values, masked secrets, runtime constants. Never mutates state

Adding a new feature

  1. Create features/<name>/ with its HTTP handlers + Templ components.
  2. Wire it in internal/server/chi.go — build the handler in BootChi, mount the routes in buildChiMux.
  3. Use goqite for async work, the SSE Hub for user-facing feedback, and Datastar for the reactive frontend.
  4. Add a SCOPE: annotation — make check-scope enforces it.
  5. Add a Routes() method on your handler and mount it in buildChiMux.

The Todo feature is the full reference implementation.

The surfaces you get

SurfaceURLNotes
Landing page/Public. Same page for guests and signed-in users
Todo demo/todoAuth-gated. Seeded demo account
Ask AI demo/genuiAuth-gated. Keyless demo answers; live model with GOAI_API_KEY
Chat demo/chatAuth-gated. Persisted per-owner history; model-invented markup (sanitized) with catalog fallback
Review loop/reviewAuth-gated. Writer → critic → classifier judge, streamed verdict, discarded. Runs on the genkit plugin
Whiteboard/whiteboardAuth-gated
Room demo/roomAuth-gated. Heartbeat roster + presenter lock owned by a GoAkt grain
Config view/configAuth-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.

FeatureFilesRawGzip
Todo, Room, Landing, Config, Ask AI, Chatinline only (no extra JS)0 KB0 KB
Shared notesnotes.js28 KB10 KB
Whiteboardwhiteboard.js + rough.min.js56 KB19 KB
Shared core (every page)datastar.js + theme.js40 KB16 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.