Skip to Content

gofasta dev

One-command development loop. Runs the full pipeline: start compose services, wait for healthchecks, apply pending migrations, launch Air for hot-reload, stream structured events for agents, accept interactive keyboard controls, and tear everything down on Ctrl+C.

Usage

gofasta dev [flags]

What runs by default

gofasta dev activates the cache and queue compose profiles automatically so your scaffolded project’s redis + asynqmon services come up alongside the database. Use --services=<list> to pick an explicit subset (e.g. --services=postgres to skip cache + queue), or the interactive preflight menu to opt out of specific services without typing the list.

A second mode, --all-in-docker, runs Air inside the app container and streams its hot-reload output to the foreground — useful when you want full Docker isolation but still want to see live logs.

While the dev loop is running, the terminal accepts single-key shortcuts — r to restart everything from scratch, q to quit, h for help.

Pipeline

Each stage can be opted out independently.

  1. Preflight — verify docker and docker compose availability.
  2. Fresh volumes (optional, --fresh) — drop every compose volume before starting.
  3. Service start — docker compose --profile cache --profile queue up -d <resolved services>.
  4. Health-wait — poll each service’s compose healthcheck until healthy (timeout 30s by default).
  5. Migrate — migrate up against the now-healthy database. Skipped under --all-in-docker — the dev container runs migrate itself before Air.
  6. Seed (optional, --seed) — run seeders after migrations.
  7. Air — exec go tool air on the host against .air.toml. Under --all-in-docker, instead tail the app container’s stdout via docker compose logs -f app.
  8. Teardown — on SIGINT/SIGTERM or q, stop services (volumes preserved by default). On r, re-run the entire pipeline from scratch.

On projects without a compose.yaml, steps 1–4 are short-circuited and the pipeline falls straight to Air — preserving the “I’m bringing my own DB” flow.

Flags

FlagDefaultBehavior
--all-in-dockerfalseRun the entire stack (including Air) inside Docker. Supporting services run detached; the app container’s stdout streams to the foreground for live hot-reload logs. See the dedicated section.
--services=<list>(all non-app services in compose.yaml)Comma-separated explicit list of compose services to start. Use this to opt out of specific services (e.g. --services=cache,queue to skip the DB when you’re running it elsewhere). When set, the interactive preflight menu is bypassed.
--profile=<name>(none)Additional compose profile to activate. cache and queue profiles are auto-activated when present.
--no-migratefalseSkip migrate up after services become healthy.
--no-keyboardfalseDisable the interactive keyboard layer. Use this when stdin is piped or for non-interactive sessions. Auto-disabled when stdin is not a TTY.
--no-teardownfalseLeave compose services running on exit (default: stop them).
--keep-volumestruePreserve named volumes on teardown. Pass --keep-volumes=false to run docker compose down -v instead of docker compose stop.
--freshfalseDrop every compose volume before starting — forces a clean database state.
--env-file=<path>.envAlternate env file to load before Air.
--port=<n>(from config)Override the PORT env var passed to Air / the app binary.
--rebuildfalseClear Air’s tmp/ build directory before starting so the next build is fresh. Host-Air-only; no-op under --all-in-docker.
--seedfalseRun seeders after migrations (equivalent to gofasta seed afterward).
--dry-runfalsePrint the resolved plan and exit without touching anything.
--attach-logsfalseStream docker compose logs -f for every service alongside Air. Under --all-in-docker this multiplexes db/cache/queue alongside the app’s foreground logs.
--dashboardfalseStart the local dev dashboard — an HTML debug page with routes, health, and live service state.
--dashboard-port=<n>9090Port for the dev dashboard HTTP server.
--jsonfalse (persistent)Emit structured NDJSON events instead of human log lines.

Service selection

Three ways to control which compose services come up, in priority order:

  1. --services=a,b,c — explicit list. Starts exactly those services and skips the preflight menu. Best for scripts.
  2. Interactive preflight menu — when stdin is a TTY and --services isn’t set, gofasta prints the resolved service list with options to (1) launch as-is, (2) deselect specific services, (3) run without DB (degraded mode with a SQLite stub), or (q) quit. See preflight menu below.
  3. Default — every non-app service in compose.yaml plus the cache and queue profiles if defined. The app service is included only under --all-in-docker.

Preflight menu

When you run gofasta dev on an interactive terminal without --services=<list> (and without --no-keyboard), gofasta resolves the compose service list and prints a menu before launching anything:

gofasta dev — preflight Services to start: postgres, redis, asynqmon [1] Launch as-is [2] Deselect specific services (then launch) [3] Run without DB (degraded mode — uses a SQLite stub) [q] Quit Choice [1]:

Pressing Enter (or 1) launches with the default set. [2] opens a per-service toggle so you can drop e.g. asynqmon without retyping the full list. [3] enters degraded mode — gofasta starts the cache + queue services (if any) but skips the database, then injects a per-process SQLite stub so the app boots and Air can hot-reload without a database connection. Useful for UI / routing work where you don’t need real data.

The menu auto-skips when:

  • --services=<list> is set — the explicit list wins, no menu shown
  • stdin is not a TTY (CI, piped input) — default service list is used
  • --no-keyboard is set — no menu, no in-loop keystroke handling

Interactive keyboard controls

While the dev loop is running, gofasta puts stdin in raw mode and watches for these single-key shortcuts:

KeyAction
r / RRestart the entire pipeline from scratch. Services stop, then every stage re-runs (.env reload → preflight → start services → wait healthy → migrate → seed → Air). Same effect as Ctrl+C and gofasta dev again, without leaving the process.
q / QQuit cleanly. Equivalent to Ctrl+C — runs the existing teardown flow.
h / H / ?Print the keybinding help inline.

When gofasta dev starts with the keyboard layer active you’ll see a one-line banner:

⌨ press `r` to restart · `q` to quit · `h` for help

The keyboard listener auto-disables when stdin is not a TTY (CI runs, piped input). Pass --no-keyboard to disable it explicitly when stdin is needed by something else.

When you’d press R. Air handles Go-source reloads automatically — you don’t need R for that. Press R when something outside the Air-watched filesystem changes:

  • You edited .env or config.yaml and want the new values picked up.
  • You added or rebased migration files and want them re-applied from a clean state with --fresh.
  • A compose service got into a weird state and you want a fresh up -d cycle.
  • You want to test cold-start behavior (full pipeline replay) without exiting the process.

—all-in-docker mode

Adds the app compose service to the orchestrated set so Air runs inside the app container rather than on the host. Supporting services (db, cache, queue) still run detached; the app container’s stdout (Air’s hot-reload output) is streamed to your terminal via docker compose logs -f app, so the experience matches host-side Air.

gofasta dev --all-in-docker

What changes vs. the default mode:

StageDefault--all-in-docker
Service startcompose up -d <db, cache, queue>compose up -d <app, db, cache, queue>
MigrateHost runs migrate up against localhost:5433Skipped on host — the dev container’s CMD runs migrate against db:5432 before starting Air.
Airgo tool air on the hostAir runs inside the app container; gofasta tails its logs to the foreground.
JSON event{"event":"air","status":"running"}{"event":"air","status":"running-in-docker"}

Mutual exclusions. --all-in-docker requires the database service in --services=<list> (the in-container app needs the database) and requires an app service in compose.yaml — gofasta scaffolds one by default; if your project’s compose file is hand-authored without one, a DEV_FLAG_CONFLICT error fires.

Multiplexing other service logs. By default only the app’s logs reach the foreground. Pass --attach-logs alongside --all-in-docker to also stream db / cache / queue logs (compose prefixes each line with the service name).

Structured output (--json)

Pipe gofasta dev --json into jq or your CI. One NDJSON event per line:

{"event":"preflight","status":"ok","docker":"28.0.1","compose":"v2.26.0"} {"event":"service","name":"db","status":"starting"} {"event":"service","name":"db","status":"healthy","duration_ms":2840} {"event":"migrate","status":"ok","applied":3} {"event":"air","status":"running","port":8080,"urls":{"rest":"http://localhost:8080","metrics":"/metrics","swagger":"/swagger/index.html","health":"/health"}} {"event":"shutdown","teardown":"stopped","exit":0}

Under --all-in-docker the air event carries "status":"running-in-docker" instead of "running" so consumers can branch on the runtime:

{"event":"migrate","status":"skipped","message":"running inside the app container"} {"event":"air","status":"running-in-docker","port":8080,"urls":{"rest":"http://localhost:8080","health":"/health"}}

Error codes

CodeWhen
DEV_DOCKER_UNAVAILABLEdocker not on $PATH or daemon unreachable
DEV_COMPOSE_NOT_FOUNDNo compose.yaml but --services or --all-in-docker was set
DEV_SERVICE_UNHEALTHYA service didn’t become healthy within the compose healthcheck deadline
DEV_MIGRATION_FAILEDmigrate up returned non-zero
DEV_AIR_NOT_INSTALLEDgo tool air not registered in go.mod
DEV_PORT_IN_USEThe configured PORT is already bound
DEV_FLAG_CONFLICTTwo flags asked for incompatible behavior — e.g. --all-in-docker against a compose.yaml with no app service.

Each carries a hint and docs field in the JSON error payload — agents can branch on the code and surface the hint verbatim.

The dashboard

gofasta dev --dashboard starts a tiny HTTP server on :9090 (configurable via --dashboard-port) that serves a live HTML debug page:

  • Current app health (polled every 5s against /health)
  • Running compose services + their state
  • Registered REST routes (scraped from docs/swagger.json, including request/response Go types)
  • Direct links to /swagger, /graphql, /metrics when the project exposes them
  • Prometheus metrics (request totals, in-flight gauge, average latency)
  • Recent requests ring buffer (method, path, status, duration, trace ID, replay button)
  • Recent SQL queries with rows affected and duration
  • Per-request trace waterfall with nested spans, stack snapshots, and span events

Updates stream over Server-Sent Events at /api/stream; the JSON snapshot is at /api/state. The dashboard dies with the rest of the pipeline on Ctrl+C.

Full-request inspection (Levels 1–4)

When the app is built with the devtools build tag (gofasta dev sets this automatically via GOFLAGS), the dashboard stops being a read-only view and becomes an interactive inspector:

Level 1 — Trace waterfall. An OpenTelemetry SpanProcessor registered by devtools.RegisterTraceProcessor() snapshots every span into an in-memory ring keyed by trace ID. Click a trace row to see a waterfall of nested spans — controller → service → repository → SQL — with offsets and durations drawn to scale.

Level 2 — Stack snapshots per span. At span start, the processor records up to 20 call frames (runtime.Callers) so the dashboard can show exactly where in your source a span was opened. Click the ▸ beside a span to reveal its stack.

Level 3 — Auto-instrumented service/repository layers. The scaffold’s generators emit otel.Tracer().Start(ctx, "Service.Method") at the entry of every generated service and repository method. Any resource scaffolded with gofasta g scaffold contributes spans to the waterfall out of the box — no manual instrumentation needed.

Level 4 — Request replay. Every captured request keeps its body (capped at 64 KiB) in the ring. The Replay button on any row POSTs that request back to the live app. Mutating methods (POST / PUT / PATCH / DELETE) prompt for confirmation first. The response body and status appear inline so you can iterate on a bug without re-clicking through the UI.

All four levels are no-cost in production: the devtools package ships a stub (//go:build !devtools) whose middleware is a pass-through and whose RegisterTraceProcessor is a no-op. go build without -tags devtools dead-code-eliminates every debug surface.

Deep-inspection features

The dashboard isn’t just a live view — it’s an interactive inspector covering the full request lifecycle. Everything below is gated behind the devtools build tag (set automatically by gofasta dev); production builds compile the stub and pay zero cost.

Go runtime profiles (pprof). net/http/pprof is mounted under /debug/pprof/ whenever the app runs with the devtools tag. The dashboard surfaces a Profiles panel with one-click links to CPU (30s), heap, goroutine, mutex, block, allocations, and execution trace profiles. Open them in go tool pprof or view inline via the browser UI.

Goroutine inspector. The dashboard parses /debug/pprof/goroutine?debug=2 and groups live goroutines by top-of-stack function — click to expand per-bucket stacks. Spotting a runaway goroutine leak is a single refresh away.

N+1 query detection. Every captured SQL statement carries its trace ID and a normalized template (literals replaced with ?). The dashboard groups by (trace, template); any trace with ≥3 duplicate templates surfaces as an N+1 finding with the offending template shown. Click the trace link to see exactly which request caused it.

EXPLAIN on click. Each captured SELECT gets an EXPLAIN button. Clicking it opens a modal that ships the SQL + captured parameter values to the app’s /debug/explain endpoint — the app runs EXPLAIN against GORM and returns the plan. A SELECT-only whitelist prevents accidentally re-executing DML.

Per-request log viewer. devtools.WrapLogger decorates the project’s *slog.Logger so every record is teed into an in-memory ring keyed by trace ID. Expand any trace row → switch to the Logs tab → see exactly which log lines that request emitted, with levels and attributes.

Panic + exception history. devtools.Recovery wraps pkg/middleware.Recovery. Every recovered panic pushes an entry into the exceptions ring — recovered value, full stack, originating method + path, and the trace ID of the failing request. The Exceptions section surfaces the last 50 entries.

Cache hit-miss log. devtools.WrapCache decorates cache.CacheService so every Get/Set/Delete/Flush/Ping is recorded with op, key, hit/miss flag, and duration. Click a trace ID to see which cache ops happened during that request.

Queue inspector. When the scaffold’s asynqmon compose service (profile=queue) is running, the dashboard detects it and surfaces a direct link in the App cards row.

Edit-and-replay. The single-click Replay has been upgraded to an inline editor — click Replay, tweak the captured body in a textarea, hit Send. Response status + body appear inline; mutating methods prompt for confirmation first.

HAR export. The Export HAR button in the Recent Requests header downloads the current ring as HAR 1.2 JSON, importable into Chrome DevTools, Insomnia, Postman, or any HAR-aware viewer.

Dashboard endpoints

The dashboard HTTP server also exposes a small JSON API for agents and CI hooks:

EndpointPurpose
GET /HTML page with server-side-rendered initial state and an SSE subscriber for live updates
GET /api/stateCurrent dashboardState snapshot as JSON
GET /api/streamServer-Sent Events stream (5s cadence)
GET /api/trace/{id}Full TraceEntry — every span, stack, event, attribute
GET /api/logs?trace_id=<id>&level=<INFO>Slog records filtered by trace / level
POST /api/replayRe-fire a captured request. Body: { "method": "POST", "path": "/api/v1/orders", "body": "..." }
POST /api/explainRun EXPLAIN against the registered GORM DB. Body: { "sql": "SELECT ...", "vars": ["a", "b"] }
GET /api/harDownload the current request ring as HAR 1.2 JSON

Scaffold-side debug endpoints

These live on the app itself (mounted under /debug/ by devtools.Handler()), not on the dashboard. The dashboard scrapes them on a 5s refresher.

EndpointPurpose
GET /debug/requestsCaptured RequestEntry ring (method, path, status, duration, trace ID, body, response body)
GET /debug/sqlCaptured QueryEntry ring (SQL, vars, rows, duration, trace ID)
GET /debug/tracesSummary list of completed trace waterfalls
GET /debug/traces/{id}One full trace including every span, stack, event
GET /debug/logs?trace_id=&level=Filtered slog ring
GET /debug/errorsRecent panics with stacks
GET /debug/cacheRecent cache ops
POST /debug/explainEXPLAIN a SELECT
GET /debug/pprof/*Standard Go profiling endpoints
GET /debug/health{"devtools":"enabled"} or {"devtools":"stub"}

Recipes

# Everything up — db, cache (redis), queue (asynqmon), Air on the host. Ctrl+C to stop. gofasta dev # Just the database — drop cache + queue via the explicit services list. gofasta dev --services postgres # Run the entire stack inside Docker. Air runs in the app container; its # logs stream to your terminal so the experience matches host-Air. gofasta dev --all-in-docker # --all-in-docker plus multiplexed db/cache/queue logs in the same terminal. gofasta dev --all-in-docker --attach-logs # Bring up services but run Air pointing at a different port. gofasta dev --port 8090 # Host-run app with your own Postgres — bypass compose entirely (empty services list). gofasta dev --services '' # Debug Air itself — no compose services, no migrations, just the rebuild loop. gofasta dev --services '' --no-migrate # Start with a fresh, seeded database every time. gofasta dev --fresh --seed # Add an extra profile beyond the auto-on cache + queue. gofasta dev --profile observability # Dev dashboard + log streaming + JSON mode for a CI run. gofasta dev --dashboard --attach-logs --json # Print the plan without running it — useful in CI sanity checks. gofasta dev --dry-run --json # Disable interactive keyboard (e.g. when a parent process owns stdin). gofasta dev --no-keyboard

Restart workflow

Once the dev loop is running, you don’t need to exit it to re-run from scratch — press r at any time. The pipeline:

  1. SIGINTs Air (or sends teardown to the app container under --all-in-docker).
  2. Runs the configured teardown — compose stop by default, or compose down -v if --keep-volumes=false.
  3. Re-loads .env (so edits to it apply).
  4. Re-runs every stage from preflight through Air.

A short banner is printed between iterations:

⟳ restarting from scratch (iteration 2)

If the keyboard layer is disabled (non-TTY or --no-keyboard), restart is unavailable — exit and re-invoke gofasta dev instead.

Last updated on