The problem it exists for
Three tools in the estate have a full web UI. keryx’s studio is where reels and social posts get made, krites’ studio is where a photo shoot gets culled, and Scout.DM’s portal is where a DM runs a campaign. Each one ships as a single Go binary, and the UI has to travel with it: no separate web server to install, no Node on the user’s machine, nothing to keep in step. Each one is also a front end onto a tool that already works from the command line, so the UI mustn’t become a second place where behaviour lives.
How it works
Svelte, not React or Angular
The choice was made for krites first, and written down there. Svelte “compiles to tiny vanilla JS, no virtual-DOM runtime”, which keeps the footprint and memory low and embeds cleanly in a Go binary, and the spec’s advice is to “Avoid a heavyweight React/Next stack for a localhost tool” (krites spec 0001). Vue was weighed and lost on footprint, and pure-Go native toolkits were turned down as an unfamiliar stack with a hand-built grid. The API in front of the UI doesn’t care which framework calls it, so “the choice stays reversible”.
keryx took the same stack from krites, and Scout.DM chose Svelte for its portal too. All three now run Svelte 5 on Vite 8, with Svelte’s runes for state rather than stores.
Plain Svelte first, SvelteKit where it earns it
keryx and krites are single-page apps in plain Svelte, with no router. keryx’s three screens are “conditional views”, and a router is to be reconsidered only “if state-sharing pain appears” (keryx spec 0011).
Scout.DM’s portal is bigger: more pages, two separate layouts, sign-in, and it’s reachable from outside the machine. So before choosing, the same four-route app was built both ways under the portal’s content security policy. Plain Svelte needed about 60 hand-written lines of routing, loading, focus handling and error pages for four routes, “and they shipped with a bug”: the old page stayed on screen showing the new page’s data. SvelteKit handled all of that itself and needed two hashes added to the server’s policy. So the portal is SvelteKit 3 with the static adapter, built to plain files with no server-side code, and a guard script refuses any SvelteKit file that would need a server to run.
Compiled into the binary
Each UI is built to static files and compiled into its Go binary. A placeholder page is committed in their place, so the binary still builds on a machine with no Node, and the placeholder tells whoever sees it to install a release. The release pipeline refuses to ship it. keryx found out why the hard way: without that guard, a build image missing Node “would build, sign, notarize and publish a release serving the ‘install a release for the full UI’ placeholder, entirely green”, and no Go test would notice, because they pass the same either way. cicd’s Svelte components build, lint, test and scan each UI, and the release waits for the build.
One API, typed at both ends
keryx and Scout.DM describe their API once, in OpenAPI, and generate both the Go server’s code and the browser’s TypeScript types from that one file. A check fails when the committed types fall behind it. krites documents its API by hand instead (studio API).
None of the three adds behaviour of its own. krites requires that the studio “exposes nothing the CLI/engine can’t do”, and keryx runs the same scenarios through the studio and the command line and checks they leave the same files behind. Estate architectural patterns covers that rule across the estate.
Locked down by where it listens
krites’ studio listens on the local machine only, and refuses any other address. keryx’s is open on loopback, and anywhere else it needs a one-time token that becomes an HttpOnly, SameSite=Strict cookie served over HTTPS (keryx spec 0018). It’s a cookie rather than a header because the browser’s image, audio and video requests can’t send a header. Scout.DM’s portal, which DMs reach from anywhere, signs people in with Discord and runs under a content security policy that allows only its own files, plus two inline items admitted by hash.
A small supply chain
keryx and krites ship no runtime npm dependencies at all. krites wrote its own photo-grid virtualiser rather than take a library, because “a compromised JS dependency ships as malware inside an Apple-notarized app” (krites spec 0014). Development dependencies are pinned to exact versions, and cicd’s security component checks every UI for known vulnerabilities, risky code and package provenance.
Tested in a real browser
Components are tested with Vitest and testing-library, and types with svelte-check. The two studios run Playwright against a mocked API, nine spec files each, and krites’ include a run with a 4,000-frame shoot to prove the grid only ever draws a small window of it. Scout.DM’s portal is driven by a headless browser against the real Go server, sending its real policy header, and the run fails on any policy violation, which is how every SvelteKit or Vite upgrade gets checked.
Decisions and what they cost
- Svelte. The smallest runtime and the cleanest fit inside a Go binary. What it cost: where a library would have been the quick answer, as with krites’ photo grid, the estate writes its own to stay at zero runtime dependencies.
- SvelteKit, measured first. What it cost: the portal builds to 172 KB against 56 KB for the plain version, and the server carries two hashes that break when SvelteKit moves a file, which it did in 3.0.
- Generated, not committed. keryx builds its UI as part of Go’s generate step rather than committing the built files. What it cost: a release needs Node, and krites can’t be installed with
go install, which skips the step and embeds the placeholder. - Types from one file. What it cost: the generated types cover the shapes of requests and responses, not the URLs, so a mistyped path is still a runtime error. Closing that would need a library at runtime, “and the SPA ships zero runtime dependencies”.
Proof in use
- keryx, krites and Scout.DM, all on Svelte 5.57 and Vite 8, with Scout.DM’s portal on SvelteKit 3.
- All three build, lint and test through cicd’s Svelte components, and keryx and krites run its security scan too.
- Nine Playwright spec files in each studio, and a headless policy check on the portal.
Use it when, and when not to
Use it as a model if you ship a Go tool with a web UI and want it to stay one binary, one API and one set of behaviour. The placeholder-plus-release-guard trick works for any embedded front end, whatever the framework.
It’s built for UIs that sit in front of a tool, not for hosted web applications. All three are static builds with no server-side rendering, by design.
Where it’s going
New web UIs start on SvelteKit, carrying over the embedding, the typed API and the browser checks from all three, with plain Svelte still there for a tool small enough not to need it.
Last reviewed .