apistockdocs
Decisions

Local development environment

ADR-0028Accepted

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.

CommandMinimal presetFull preset
aps devBuild, run, reload on change; no DockerDocker Compose: PostgreSQL + Mailpit; migrations applied; seed data on first run; build, run, reload
aps dev --observabilityAdds Grafana (grafana/otel-lgtm)Adds Grafana (grafana/otel-lgtm)

On start, aps dev prints:

text
✓ 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
RuleDecision
Docker missing (Full)Stop with a clear message: install Docker, or set DATABASE_URL to an existing PostgreSQL
PortsChecked before start; conflicts reported with the process name where available
EmailAlways delivered to Mailpit in development (ADR-0025)
TelemetryThe app always emits OpenTelemetry; exporting to Grafana only with --observability
Default adminCreated by seed on first run; the random password is printed once and never stored (ADR-0042)
ServicesDefined in the app's owned compose.yaml; aps dev never uses hidden containers
Without the CLIdocker compose up -d plus go run ./cmd/api must work
Custom dev consolev1.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.

ContextWhere PostgreSQL comes from
Generated app, aps devService 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
CIThe same official postgres image as a GitHub Actions service container
RuleDecision
ImageOfficial postgres image, pinned to one major version in every compose.yaml and CI; upgraded deliberately
BindingHost ports bound to 127.0.0.1 only (threat 11)
Host portConfigurable in .env; aps dev checks it like APP_ADDR
DataNamed volume per app, so docker compose down keeps data and down -v resets it
HealthCompose healthcheck with pg_isready; aps dev waits for healthy before migrating
Test connectionpgtest 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 seedRun by the app's Go commands (cmd/migrate, cmd/seed), never by psql
RejectedEmbedded 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