apistockdocs
v0.4 GitHub apistock.dev
Decisions/Records

Local development environment

ADR-0028Accepted (2026-09-14)

Status: Accepted (2026-09-14) · Amends: ADR-0007, ADR-0010 · Amended by: ADR-0042 (seed password printed once, never stored)

Context#

The product promise is a working API on the first try: aps new, then aps dev. Full apps need PostgreSQL and a safe email inbox; developers benefit from seeing traces and logs; first-run time and laptop resources matter. Minimal apps must run without Docker.

Options#

  1. Developers install and configure PostgreSQL, a mail catcher and observability tools themselves.
  2. Embedded binaries downloaded by the CLI (embedded PostgreSQL).
  3. Docker Compose managed by aps dev, with heavier tools opt-in.

Decision#

Option 3.

Command Minimal preset Full preset
aps dev Build, run, reload on change; no Docker Docker Compose: PostgreSQL + Mailpit; migrations applied; seed data on first run; build, run, reload
aps dev --observability Adds Grafana (grafana/otel-lgtm) Adds Grafana (grafana/otel-lgtm)

On start, aps dev prints:

output
✓ API        http://localhost:8080
✓ API docs   http://localhost:8080/docs
✓ Emails     http://localhost:8025
  Google login: not configured → docs/auth-providers.md
  Tip: aps dev --observability to see traces and logs
Rule Decision
Docker missing (Full) Stop with a clear message: install Docker, or set DATABASE_URL to an existing PostgreSQL
Ports Checked before start; conflicts reported with the process name where available
Email Always delivered to Mailpit in development (ADR-0025)
Telemetry The app always emits OpenTelemetry; exporting to Grafana only with --observability
Default admin Created by seed on first run; the random password is printed once and never stored (ADR-0042)
Services Defined in the app's owned compose.yaml; aps dev never uses hidden containers
Without the CLI docker compose up -d plus go run ./cmd/api must work
Custom dev console v1.1; may replace Grafana for local viewing

Why#

Docker Compose gives production-like PostgreSQL with no manual setup; making Grafana opt-in keeps the first run fast and light.

Trade-offs#

  • Full apps require Docker for the zero-setup path.
  • Observability isn't visible until the developer opts in.

Consequences#

  • First-run spike: Minimal from clean caches in 12.0 s (build CLI, aps new, build, /docs ready), 1.6 s with warm caches. Target under 60 seconds met.
  • Full preset timing (including Docker image pulls) is measured in v0.2 and documented.

PostgreSQL always runs in Docker (2026-09-14)#

Development, tests and CI all get PostgreSQL from a Docker container. apistock never downloads or embeds PostgreSQL binaries and never requires a locally installed postgres or psql.

Context Where PostgreSQL comes from
Generated app, aps dev Service postgres in the app's owned compose.yaml, started by aps dev (or docker compose up -d)
Generated app tests (repository/, test/e2e) The same Compose database; pgtest creates a throwaway database per test package from a migrated template
apistock repository (module tests, examples) compose.yaml at the repository root; docker compose up -d --wait
CI The same official postgres image as a GitHub Actions service container
Rule Decision
Image Official postgres image, pinned to one major version in every compose.yaml and CI; upgraded deliberately
Binding Host ports bound to 127.0.0.1 only (threat 11)
Host port Configurable in .env; aps dev checks it like APP_ADDR
Data Named volume per app, so docker compose down keeps data and down -v resets it
Health Compose healthcheck with pg_isready; aps dev waits for healthy before migrating
Test connection pgtest reads APISTOCK_TEST_DATABASE_URL; when it is unset the test is skipped with the exact docker compose up command, and CI sets APISTOCK_REQUIRE_DB=1 so a missing database fails instead of skipping
Migrations and seed Run by the app's Go commands (cmd/migrate, cmd/seed), never by psql
Rejected Embedded PostgreSQL downloads (option 2 above) and testcontainers (a Docker API client dependency in every app, and hidden containers the developer can't see in compose.yaml)

v0.1 implementation notes#

  • aps dev loads .env into the app's environment; variables already set in the real environment win. The app itself has no dotenv dependency.
  • Before starting, aps dev checks that APP_ADDR (default 127.0.0.1:8080) is free and, if not, stops with a message suggesting another APP_ADDR.
  • Reload polls watched files (Go sources, module files, .env, .html, .json, .sql) every 500 ms. A failed build keeps the previous version running. The app runs in its own process group so Ctrl+C stops it exactly once.
  • Measured with the real CLI (scripts/first-run.sh): 25.0 s from clean caches (196 MB of modules, mostly OpenTelemetry exporter dependencies), 4.8 s warm. Target met.

v0.2 implementation notes (2026-09-15)#

  • aps dev reads features in apistock.yaml. Apps with postgres take the Docker path; Minimal apps still build and run without Docker unless --observability is given.
  • Before the first start: .env is created from .env.example (mode 0600) when missing; docker compose version and docker compose ps --services --status running check Docker; the host ports of services that aren't already running are checked (POSTGRES_PORT, MAILPIT_SMTP_PORT, MAILPIT_WEB_PORT, and with --observability GRAFANA_PORT and OTLP_HTTP_PORT), a taken port naming the .env line that moves it; docker compose up -d --wait; go run ./cmd/migrate; go run ./cmd/seed when the app has it (ADR-0042); then the banner.
  • --no-services skips Docker and uses the addresses in .env; migrations and seed still run. Without Docker, the error offers that path.
  • Since ADR-0043, aps dev also fills an empty AUTH_ENCRYPTION_KEYS in .env with dev:<random 32-byte key> (and keeps the file at mode 0600), only for apps whose .env.example declares the variable and when the environment doesn't set it, so seed data can enroll the administrator in two-factor authentication.
  • While running, a changed or new .sql file under db/migrations runs migrations after a successful build and before the restart; a failed migration keeps the previous version running.
  • Services are left running when aps dev stops, so restarts are fast; docker compose down stops them.
  • --observability: Grafana is the grafana/otel-lgtm:0.33.0 service behind the observability Compose profile in each preset's owned compose.yaml (the Minimal preset gains a compose.yaml holding only it), bound to 127.0.0.1. aps dev sets OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<OTLP_HTTP_PORT> for the app process only, over any .env value.
  • The banner's "Google login: not configured" line arrives with social login (v0.3).
  • Tests: the order of commands, .env creation, port conflicts, missing Docker and --no-services run with fake commands; APS_E2E_DOCKER=1 runs aps dev in a new Full app against real Docker, signs in as the seeded administrator and finds a registration email in Mailpit.
  • Measured with that test (2026-09-15, Docker Desktop on macOS, images and Go caches warm): the API answers /readyz 9.6 s after aps dev starts, including Compose health waits, migrations and seed data. Runs that pull images depend on the network and aren't measured.
esc
↑↓ move↵ openesc close