Skip to content

The Svelte frontend track

Go projects built on the go-tool-base pattern (keyrx, haileys-app) embed a Svelte web UI directly into the Go binary via //go:embed. The four Svelte components — svelte-build, svelte-lint, svelte-test, svelte-security — exist to handle two coupling points that a normal Go-only pipeline doesn't have to think about, plus the ordinary frontend validation work every UI needs.

Coupling point one: the release binary needs the real bundle

The pattern looks like this: pkg/studio/web/ is a Vite/Svelte app; its built output lands in pkg/studio/web/embed/, which //go:embed all:web/embed compiles into the Go binary. A committed placeholder bundle in that directory means go build compiles standalone even before Node has ever run — but the real bundle has to exist before a release binary ships, or users get the placeholder's "install a release for the full UI" notice instead of the actual application.

Two ways to close that gap exist, and which one a project needs depends on how it already builds the bundle:

  • go:generate (the common case for keyrx and haileys-app) — a //go:generate build-web.sh directive, invoked by goreleaser's own before: hooks: - go generate ./..., builds the frontend as part of the ordinary release build. This also means a plain go build / go install works standalone for anyone with Node available locally — something a CI-only mechanism can't offer.
  • svelte-build — for a project that doesn't have that go:generate hook, this component builds the bundle as a separate CI job and hands it to the release job as an artifact: goreleaser: needs: [{ job: svelte-build, artifacts: true }].

These are not both used on the same project. A project with the go:generate hook that also wired svelte-build would build the bundle twice at tag time — once via the before-hook, once via the artifact handoff — for no benefit. svelte-build exists specifically for projects that don't have the go:generate mechanism; where it does exist, the hook is the single build path, and only svelte-lint / svelte-test / svelte-security apply.

This was a genuine correction, not the original design: the components initially assumed the artifact-handoff was the only sensible mechanism and rejected a goreleaser before-hook outright as coupling the npm build into every consumer's .goreleaser.yaml. In practice, the projects that motivated this whole track already had exactly that hook, working, before svelte-build existed — so the blanket rejection was wrong, and the guidance is now per-project rather than one-size-fits-all.

Coupling point two: a frontend-only change still needs the Go tests to run

embed_test.go (the synthetic-filesystem test asserting spaFromFS's served-UI behaviour) and the served-SPA handler both depend on what's actually inside web/embed/. But go-test's default change-detection filter only watches Go source and module files — a change confined entirely to pkg/studio/web/ wouldn't trip it. So a project with this embed pattern must extend go-test's changes input with its frontend path (pkg/studio/web/**), so a Svelte-only change still runs the Go tests that assert on the embedded bundle.

This is the one place change-detection's "consumers extend the default" escape hatch is load-bearing rather than a nicety — skip it, and a frontend-only MR could merge having never exercised the Go code that serves it.

The security posture: three layers, all free and open-source

svelte-security was built to be security-forward specifically because a web UI is a different, and in some ways larger, attack surface than a Go CLI — XSS, dependency confusion, and supply-chain risk in a way go-security doesn't need to cover. It deliberately covers three layers with entirely free/open-source tooling (a commercial behavioral scanner like Socket or Phylum was considered and explicitly declined — paywalled, and no free equivalent does the same job):

  • known vulnerabilitiesosv-scanner (which also ingests the OSV MAL- malicious-packages feed at no extra cost), npm-audit, and retire.js;
  • SAST/XSSsemgrep, tuned specifically for the web-UI risk profile (p/javascript, p/typescript, p/xss, p/secrets);
  • provenance and integritynpm audit signatures (registry signatures) and lockfile-lint (every resolved dependency URL must be HTTPS and the official registry — a dependency-confusion guard).

Like every *-security component, this whole set is always-on, not change-detected.

See also