apistockdocs
v0.4 GitHub apistock.dev
Decisions/Records

Full preset generation

ADR-0041Accepted (2026-09-15)

Status: Accepted (2026-09-15) · Amends: ADR-0014, ADR-0021, ADR-0028 · Amended by: ADR-0050 (apistock.lock v2 records the release, template inputs and file hashes)

Context#

aps new --preset=full must create an app like examples/full-single: PostgreSQL, runtime settings, jobs, email, authentication, audit, release tracking, operations APIs and the example resource. ADR-0014 requires golden reference apps that CI verifies the generator reproduces. The Minimal preset already works that way: go generate turns examples/minimal into templates by replacing its placeholder module path and name, and a hand-written go.mod template adds the library requirements.

Reading the golden app for the Full preset showed what the Minimal approach doesn't cover:

Finding Evidence Consequence if copied as is
Golden-only text README: "This app is the golden copy of what aps new --preset=full will generate" Every new app claims to be the golden copy
Links into the apistock repository README links to ../../docs/guides/authentication.md and email.md Broken links in every new app
A second, unreplaced placeholder Database user, password and name acme in compose.yaml, .env.example, README, AGENTS Every app's database is called acme
Ten apistock modules plus pgx, River and Huma in go.mod examples/full-single/go.mod A hand-written go.mod template is a second copy of the dependency list that drifts; Minimal's already lacks the indirect requirements its golden app has
Tests that need PostgreSQL pgtest skips without APISTOCK_TEST_DATABASE_URL "The generated app passes its tests" is only true with a database

Constraints: the golden app stays the single source of truth and hand-written; generated apps work without apistock tooling (architecture principle 8); template injection and writes outside the project stay impossible (threats 2 and 3); the first run stays well under the 60-second target.

Options#

1. Generate committed templates from the golden app 2. Read the golden app at run time 3. Hand-written Full templates
Shape go generate writes cli/internal/recipes/full; aps embeds it; CI regenerates and fails on a diff aps new copies examples/full-single from a checkout or download A second template tree edited by hand
Source of truth Golden app Golden app Two copies
Works offline, from a released binary Yes No: needs a checkout or a network fetch (threat 4: no downloads at install) Yes
Drift detection Byte-for-byte golden test and the CI diff None needed Manual
Binary size About 0.5 MB of templates None About 0.5 MB
Cost to reverse Low Medium High
Fits existing path Yes: the Minimal pipeline No No

Option 1.

Decision#

Templates#

  • generate.Run handles both presets: go generate ./internal/recipes/ writes minimal/ from examples/minimal and full/ from examples/full-single. Templates are never edited by hand.
  • Placeholders stay the two the Minimal preset uses: example.com/acme-api becomes the module path, then acme-api becomes the app name.
  • go.mod comes from the golden go.mod, for both presets: every require is kept, apistock.dev modules take the CLI's library version, the golden replace block is dropped, and with --local a replace is written for every apistock.dev module the golden app requires. go.sum isn't copied; aps new runs go mod tidy.
  • Leak check: generation fails when a golden file (other than go.mod) contains a path into the repository (../../) or the template delimiters. Golden apps state repository-only facts in the repository's docs, not in their own files.

Golden app changes#

  • The development database is named after the app: user, password and database acme-api, so a new app's database is its own name. The name pattern (lowercase letters, digits, single hyphens, at most 63 characters) is a valid PostgreSQL role and database name and Compose project name.
  • The README no longer calls itself the golden copy, and names the apistock guides instead of linking into the repository.

aps new#

Topic Decision
Presets --preset minimal (default) or full; custom stays unavailable. In a terminal, a select replaces the "Full arrives in v0.2" note
Recipe in apistock.lock base-full for Full, base-minimal for Minimal, with every created file's hash
Content Exactly the golden app for the name and module: example ping endpoint, heartbeat job and projects resource included, so the output is the reviewed, tested app. The README says how to remove the examples
Tenancy and email Single-tenant; Resend by default. aps add mail switches to SMTP afterwards. Tenancy and mail prompts arrive with organisations (v0.4) and the Custom preset
Next steps printed aps dev (since 2026-09-15; before that, the commands below), with cp .env.example .env, docker compose up -d --wait, go run ./cmd/migrate, go run ./cmd/seed and go run ./cmd/api as the path without the CLI; the Mailpit inbox; the seeded administrator; POSTGRES_PORT and DATABASE_URL when port 5432 is taken
aps dev with Docker A separate v0.2 item, done 2026-09-15: see ADR-0028's v0.2 implementation notes

Verification#

Level Check
Templates Golden test per preset: rendering with the placeholders reproduces every golden file byte for byte, and nothing else
Generator Unit tests for the go.mod derivation and the leak check
CLI aps new --preset full writes the files, lock recipe and module path, and leaves no placeholder or template syntax
End to end (APS_E2E=1) aps new --preset full --local, then go vet and go test in the new app (database tests run when APISTOCK_TEST_DATABASE_URL is set), then aps gen resource, aps gen job and aps gen migration in it, with a column added to the generated resource's table in that migration, then go vet and go test again
CI Regenerates both template trees and fails on a diff; the end-to-end job gets a PostgreSQL service so the generated app's database tests run

Why#

  • One source of truth: the app developers read, run and review in this repository is the app they get.
  • Deriving go.mod removes the one hand-maintained copy of the dependency list, for both presets.
  • Fixing leaks in the golden app, plus a check, costs less than new placeholders and keeps templates simple.
  • The end-to-end test proves the parts compose: a new Full app accepts the generators users run next.

Trade-offs#

  • New apps include example code (ping, heartbeat, projects) to delete or adapt.
  • aps embeds about 0.5 MB more templates.
  • Every golden change regenerates templates in the same pull request; CI enforces it.
  • Port 5432 is often taken on developer machines; until aps dev checks ports, the printed next steps explain the override.
  • Existing local volumes of examples/full-single were created with the old acme role; docker compose down -v resets them.

Consequences#

  • aps new --preset=full is part of the v0.2 definition of done: register → verify → login → role-protected endpoint → audit event are covered by the generated app's own tests.
  • Template changes to either preset follow ADR-0016's upgrade rules from the first release.
  • Recipe name base-full is recorded in apistock.lock and is public (ADR-0015).

Results (2026-09-15)#

Check Result
Templates 128 files, about 0.8 MB; both presets reproduce their golden apps byte for byte; the rendered go.mod requires and replaces exactly what each golden go.mod does
New Full app, warm caches, local checkout aps new --preset full including go mod tidy: 1 s; go build ./...: 3 s; go test ./... with APISTOCK_REQUIRE_DB=1 (every database test runs, including the authentication, projects and releases end-to-end tests): 8 s
End to end New Minimal and Full apps pass go vet and their tests; aps gen resource and aps gen job in the new Full app leave it vetting cleanly
Not measured Cold caches and Docker image pulls, which depend on the network; measured with aps dev and Docker
Found while implementing A --local path containing a space produced an invalid go.mod for both presets; replace paths are now quoted when needed
esc
↑↓ move↵ openesc close