Dev-mode livereload
framework.NewApp() and uihost.New() auto-wire a tiny SSE-based
livereload pair when the process is running under gofastr dev. Edit
a watched file (.go, .js, .css, .html, or .md — the
extensions Go embeds), the binary restarts, and every open browser tab
refreshes on its own — no host-app code required.
The rebuilt server runs with the project directory (--dir) as its
working directory — the same cwd it gets when run by hand — so relative
paths like a sqlite db_url or a static dir resolve against the
project, and worktree isolation keys off the project's location rather
than wherever gofastr dev was launched.
How it turns on
Three env vars decide. All defaults are dev-friendly; production is
always safe:
| Var | Default | Meaning |
|---|---|---|
GOFASTR_DEV | unset | gofastr dev sets this to 1 on the child process. Without it, livereload stays dormant. |
GOFASTR_ENV | unset | Set to production to force livereload off even if GOFASTR_DEV slips through. Belt-and-suspenders for accidental dev binaries in prod. |
GOFASTR_DEV_LIVERELOAD | unset (= on) | Set to 0 to opt out while keeping the rest of dev mode (gofastr dev's rebuild loop) running. |
Predicate (single source of truth in framework/dev/livereload.go):
GOFASTR_ENV != "production" ANDGOFASTR_DEV truthy ANDGOFASTR_DEV_LIVERELOAD != "0"
What it does
When enabled:
framework.NewApp()registers two routes on the App router:
-GET /__livereload—text/event-stream. Fires oneevent: ready
on connect, then idles with a 25s SSE-comment heartbeat. Closes
when the request context cancels (server shutdown / browser tab
close).
-GET /__livereload.js— ~16 lines of JS. Opens anEventSource,
treats the secondonopen(i.e. the reconnect after the
server drop) as the reload signal, callslocation.reload().uihost.New()auto-appends/__livereload.jsto theextraScripts
list so every rendered page links to the client script before
</body>. CSP-safe — it's a<script src="...">, no inline JS.- For every OTHER way an app serves a page —
static.Handlerfile
serving (SPA shells, exported static pages), widget-server pages,
hand-rolled handlers —framework.NewAppmounts dev-only middleware
that splices the same<script src>into responses that declare
Content-Type: text/html(no sniffing — set the type) and are full
documents (contain</body>) and don't already carry the tag.
Fragments (island RPC swaps, SPA-nav partials), compressed bodies,
HEAD/Range requests, and non-HTML responses (JSON, SSE, streams) pass
through untouched and unbuffered; a handler that Flushes mid-HTML
streams from that point on, uninjected.
One persistent SSE connection per tab, near-zero idle traffic, no
polling.
How gofastr dev wires it
cmd/gofastr/dev.go injects GOFASTR_DEV=1 into the child binary's
environment when it launches the rebuilt server. The host app doesn't
need to forward, set, or check the flag — it's transparent.
Every rebuild (the initial one and each change-triggered one) first
runs the same static accessibility lint gofastr build enforces —
findings are printed with fix hints and the rebuild is treated exactly
like a compile failure: the server doesn't start, the watcher keeps
running, fixing and saving retries. gofastr dev --no-a11y skips the
gate with the same "escape hatch, not a setting" caveat as build's
flag (see the accessibility doc).
The dev loop is also livereload for agents
The same GOFASTR_DEV gate turns on MCP for agents: framework.NewApp
mounts /mcp (skipping, with a warning, if the host hand-mounted one),
registers read-only tools for reading the running app's state
(app_routes, app_readiness, framework_docs_search, …) and tools
that change it (app_module_enable / app_module_disable); every
CRUD-enabled entity serves its MCP data tools without per-entity
mcp: true; and battery/log — when registered — enables its
log_recent / log_filter / log_metrics / log_set_level debug
tools. A connected agent can check what's running, read recent
requests and errors, read and write app data, and turn modules on or
off on the running dev app without extra setup. See
agent-ready. Opt out with GOFASTR_DEV_MCP=0.
Opting out
# Keep rebuild-on-save but disable browser refresh:GOFASTR_DEV_LIVERELOAD=0 gofastr dev# Keep rebuild + refresh but disable the dev MCP tools:GOFASTR_DEV_MCP=0 gofastr dev
Forcing it on outside gofastr dev
For a custom watcher or air-style tool, set the env yourself:
GOFASTR_DEV=1 ./my-appThe framework will auto-register the routes and inject the script.
Browser tabs already pointing at the app reload as soon as the new
binary accepts the SSE connection.
Common mistakes
- Setting
GOFASTR_ENV=productionwhile expecting livereload to work
in dev. Production is a hard kill switch; clear the var first. - Wiring
uihost.WithExtraScripts("/__livereload.js")by hand. The
framework already does it when env says so — your manual call
becomes a duplicate<script>tag. - Registering
/__livereloadroutes by hand. The framework already
does it — a manualRouter().Getwill collide.