Decisions
Local development environment
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#
- Developers install and configure PostgreSQL, a mail catcher and observability tools themselves.
- Embedded binaries downloaded by the CLI (embedded PostgreSQL).
- 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:
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| 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 |
| 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,/docsready), 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 devloads.envinto the app's environment; variables already set in the real environment win. The app itself has no dotenv dependency.- Before starting,
aps devchecks thatAPP_ADDR(default127.0.0.1:8080) is free and, if not, stops with a message suggesting anotherAPP_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 devreadsfeaturesinapistock.yaml. Apps withpostgrestake the Docker path; Minimal apps still build and run without Docker unless--observabilityis given.- Before the first start:
.envis created from.env.example(mode 0600) when missing;docker compose versionanddocker compose ps --services --status runningcheck Docker; the host ports of services that aren't already running are checked (POSTGRES_PORT,MAILPIT_SMTP_PORT,MAILPIT_WEB_PORT, and with--observabilityGRAFANA_PORTandOTLP_HTTP_PORT), a taken port naming the.envline that moves it;docker compose up -d --wait;go run ./cmd/migrate;go run ./cmd/seedwhen the app has it (ADR-0042); then the banner. --no-servicesskips Docker and uses the addresses in.env; migrations and seed still run. Without Docker, the error offers that path.- Since ADR-0043,
aps devalso fills an emptyAUTH_ENCRYPTION_KEYSin.envwithdev:<random 32-byte key>(and keeps the file at mode 0600), only for apps whose.env.exampledeclares 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
.sqlfile underdb/migrationsruns migrations after a successful build and before the restart; a failed migration keeps the previous version running. - Services are left running when
aps devstops, so restarts are fast;docker compose downstops them. --observability: Grafana is thegrafana/otel-lgtm:0.33.0service behind theobservabilityCompose profile in each preset's ownedcompose.yaml(the Minimal preset gains acompose.yamlholding only it), bound to 127.0.0.1.aps devsetsOTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<OTLP_HTTP_PORT>for the app process only, over any.envvalue.- The banner's "Google login: not configured" line arrives with social login (v0.3).
- Tests: the order of commands,
.envcreation, port conflicts, missing Docker and--no-servicesrun with fake commands;APS_E2E_DOCKER=1runsaps devin 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
/readyz9.6 s afteraps devstarts, including Compose health waits, migrations and seed data. Runs that pull images depend on the network and aren't measured.
Was this page useful?Open an issue