Development
Lazydev Mode
Understand the development-only build tag, control-plane endpoints, and panel runtime used by lazy.
What lazydev means
The default lazy development command builds your app with the lazydev build
tag. That tag enables local-only behavior such as disk-backed views, logical
asset paths, detailed error pages, editor opening from error frames, route
inspection, and view-cache reloads.
Production builds do not include lazydev handlers.
Development control plane
When lazy starts the app, it also passes CONTROL_PLANE_ADDR so the app can
serve development endpoints on an internal control-plane address:
GET /routes
GET /buildinfo
GET /dependencies
GET /dependencies/shutdown
POST /dependencies/shutdown
GET /dependencies/shutdown/events
GET /cache
GET /cache/entry
POST /cache/on
POST /cache/off
GET /jobs
POST /views
POST /_golazy/views/reload
POST /_golazy/open-editor
GET /routes returns the registered route table. GET /buildinfo returns the
running app's Go module build information from runtime/debug.ReadBuildInfo,
including Go version, command path, main module, dependencies, replacements,
and build settings such as VCS metadata when the toolchain recorded them.
GET /dependencies returns the lazydeps application service graph with the
app root, service nodes, and directed dependency edges. The shutdown
endpoints expose a lazydev-only simulation: POST /dependencies/shutdown can
delay for test traffic, mark /readyz not ready, report active requests and
connections, wait for active requests to finish, and cancel services while
GET /dependencies/shutdown/events streams each state change.
GET /cache returns cache
enabled state, stats, keys, and inspectable entry metadata exposed for
development inspection. GET /cache/entry?key=... returns a selected entry
body when the backend supports inspection. POST /cache/on and
POST /cache/off toggle the app cache. GET /jobs
returns lazyjobs definitions, counts, and recent job state. POST /views
reloads disk-backed views without rebuilding or restarting the app. The
/_golazy/views/reload route remains as a compatibility alias.
Detailed error pages call POST /_golazy/open-editor when you click a frame.
The request includes an absolute file path and line number, and GoLazy starts
$EDITOR without waiting.
Development panel
The public development address accepts HTTP and HTTPS on the same port. Plain
HTTP serves the local HTTPS setup page until the browser trusts the GoLazy
certificate authority. HTTPS serves the panel under /_golazy/ and proxies
normal app traffic to the running app process. The panel remains available
while the app is building, failed to compile, running, or crashed.
The panel JavaScript is served as normal external assets. GoLazy does not
inline development-panel JavaScript into app HTML. Proxied app pages receive
hidden markup for a spacer, fixed bottom GoLazy development panel iframe, and
rounded-square GoLazy launcher button with the padded, yellow-backed square logo,
followed by the standalone panel.js host client. That client bootstraps
devpanel_controller.js, which shows, hides, resizes, persists the in-page
panel, and binds the launcher click to reopen it while loading /_golazy/ in
the iframe. The markup starts hidden so the page does not flash panel UI before
JavaScript runs. When the in-page panel is closed and the extension is not
installed, the controller shows the GoLazy launcher in the bottom right corner.
The launcher stays hidden when the extension is installed, the DevTools panel
is open, or the in-page panel is visible. The panel proxies cache inspection
and cache on/off requests to the app control plane so browser requests stay on
the public development origin. The host client exposes
window.disableDevPanel() so the Chrome extension can hide both the in-page
panel and launcher after the page script loads.
The served panel uses a DevTools-style shell with one compact top tab bar,
toolbar rows, dense status panes, and a bottom status bar. Each top-level tab is
its own panel page; clicking a tab performs a Turbo visit inside the panel
iframe instead of rendering every tab at once. The bottom status bar is a
permanent Turbo frame, and its status turbo-stream-source lives inside that
frame so it stays connected while moving between tabs and keeps app and service
status chips visible. The panel layout uses Turbo morph refreshes with scroll
preservation so refreshes can patch the panel document without resetting the
user's position. The app chip opens App, and service chips keep their
normal background even when selected. Existing lazy behavior is available
inside that shell: App shows service status, lazy lifecycle events, changed-file
groups, and rebuild, restart, and open-app controls; Services shows a tree with
the App service, managed services, lifecycle scripts, and all discovered mise
tasks, caps logs at 100 rows, batches live log updates, parses JSON log lines
into message and attribute columns, and exposes reload, stop, or play actions
for managed services; Jobs shows the app's lazyjobs state; Routes shows the
registered route table from the app's lazydev control plane and sends static
GET route clicks to the app through the injected host client; BuildInfo shows
the running app's Go version, module path, last build time, phase timing, and
top slow packages on the left, with runtime details, settings, and dependencies
available as right-side tabs; Requests shows captured request paths, combines
DevTools-style path search with handler filter chips that include All as the
default, and opens lazy-loaded Headers, Tracing, and Logs detail tabs;
Dependencies shows the lazydeps service graph as service rows and directed
edges, then uses Stimulus to enhance those rows into an SVG graph; Assets lists
lazy asset manifest entries and public paths; Cache shows cache usage, a
searchable key
table, selected entry content, and cache controls.
The development panel remembers the last top-level tab in session storage and
restores it when the embedded panel loads at the default App tab. Clicking a
service in the status bar opens the Services tab with that service selected.
Shared panel tables remember resized column widths in browser localStorage.
When a dragged header boundary reaches a neighboring column's minimum width,
the resize continues by compressing the next column in the same direction. When
a containing pane is resized, columns scale proportionally until they reach
their minimum widths. Grouped or multi-row table headers resize the leaf
columns they cover proportionally, so header cells with colspan do not become
separate physical columns. Rows that link into a Turbo Frame update their
selected state immediately on click.
Development request traces
The lazy development proxy assigns request correlation before forwarding app
traffic. It preserves safe incoming X-Request-ID and valid W3C traceparent
headers, or generates both when they are missing. The app telemetry middleware
uses those headers for its request id, request span, logs, metrics labels, and
the response X-Request-ID.
lazy does not force OTEL exporters into the child app. Lazydev builds install
request telemetry unless OTEL_SDK_DISABLED=true is present, but detailed
request monitoring is off by default. Enable it from the development panel to
write local files:
.tmp/traces/<request-id>.trace
.tmp/traces/<request-id>.spans
.tmp/traces/<request-id>.log.json
The .trace file is a Go runtime trace. The .spans file is JSON with request
and span metadata plus request-level allocation counts, memory deltas, system
memory, stack, GC counters, and lazydev-only per-region allocation samples. The
.log.json file is JSONL for request-local logs emitted through
lazytelemetry or lazylogs. Runtime tracing is process-wide, so lazydev
serializes traced requests while each .trace file is recorded.
The Requests tab reads the app control plane's /requests/traces snapshot,
which is built from the .spans and .log.json sidecars. Its search is
server-side path filtering through the q query parameter, and its type filter
uses type=framework, type=assets, type=other, or all requests. Request
type is derived from the last traced middleware that handled the request
without calling the next handler. The tab shows recent captured request paths,
then opens request detail tabs for Headers, Tracing, and Logs. The Tracing tab
starts with a request status strip, then a backend-sorted region table and a
flamegraph. Turn on Include golazy to include middleware, router, dispatch, and
other framework regions. Table headers sort by total or self time, allocation
count, and memory bytes; the flamegraph scale follows the selected metric.
Flamegraph bars use square classical flamegraph colors, show only the span name
inside the bar, and keep full timing and allocation details in the browser
tooltip. The region metrics table above the flamegraph keeps compact rows
without reducing text size. The flamegraph pane grows to the graph's full
height; once the table is at its minimum height, long graphs make the request
detail area scroll for inspection instead of clipping inside the flamegraph
pane.
When the sidecar includes lazydev allocation samples, those region allocation
values are process-wide runtime.ReadMemStats deltas sampled when spans start
and end, so they are development estimates rather than exact runtime.mallocgc
attribution.
Those request spans include framework child regions for middleware, routing, dispatch, controller setup, action calls, view rendering, layouts, and partials. Region names include concrete middleware, controller/action, template, and partial identifiers for Go runtime trace inspection.
Chrome DevTools extension
The chrome-extension directory contains an unpacked Chrome extension for the
development panel. It adds a GoLazy icon to the Chrome extension bar and a
GoLazy panel in Chrome DevTools.
The extension is a thin host for the lazy-served panel. When the DevTools panel
opens, it reads the inspected page URL, probes the page origin at
GET /_golazy/extension, and expects 200 OK with the exact plain-text body
i love being lazy. If the probe succeeds, the extension creates an iframe
pointed at the same origin's /_golazy/ route. If the probe fails, it shows a
local DevTools-style unavailable state with the cd project / lazy startup
snippet. The panel must fit inside the DevTools viewport and should not
duplicate the request, console, app-log, traces, routes, assets, or cache UI
served by the lazy panel app.
The probe retries during reloads and prefers the HTTPS variant when the
inspected page is briefly on an HTTP origin. Once the extension has embedded
the /_golazy/ iframe, same-origin app navigations keep that iframe mounted so
panel state survives normal clicks through the site. The extension content
script tells the inspected page that the extension is installed, suppressing the
in-page GoLazy launcher. Clicking the GoLazy extension icon toggles the
inspected page's in-page panel through that content script. Chrome extensions
cannot open DevTools itself from an action click. When the DevTools panel is
open, the extension calls window.disableDevPanel() on the inspected page when
that function is available, and falls back to the existing page message and
window.__golazyDevToolsOpen flag while the host script is still loading.
The lazy development proxy also serves Chromium's Automatic Workspace
Folders metadata at /.well-known/appspecific/com.chrome.devtools.json. The
response points workspace.root at the app's app/js folder, letting Chrome
DevTools offer a workspace mapping for browser modules without an application
route.
Chrome does not expose DevTools' private stylesheet to extension panels. The extension vendors a small adapted subset of DevTools token and class names, keeps the Chromium BSD-style license beside that CSS, and switches light/dark values through the public DevTools theme API.
Future browser-only request capture should stay in the extension host: seed
from chrome.devtools.network.getHAR(), listen to
chrome.devtools.network.onRequestFinished, and call a request's getContent()
only when response or preview content is needed. Lower-level
chrome.debugger/Chrome DevTools Protocol access should stay out of the
default extension path unless a later feature needs data the DevTools network
API cannot provide.
Read Lazy for the everyday workflow and Control Plane for production control-plane configuration.