Development

Lazydev Mode

Understand the development-only build tag, control-plane endpoints, and panel runtime used by lazy.

By Guillermo Alvarez - Published - Updated

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.