apistockdocs
Decisions

Full preset generation

ADR-0041Accepted

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:

FindingEvidenceConsequence if copied as is
Golden-only textREADME: "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 repositoryREADME links to ../../docs/guides/authentication.md and email.mdBroken links in every new app
A second, unreplaced placeholderDatabase user, password and name acme in compose.yaml, .env.example, README, AGENTSEvery app's database is called acme
Ten apistock modules plus pgx, River and Huma in go.modexamples/full-single/go.modA 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 PostgreSQLpgtest 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 app2. Read the golden app at run time3. Hand-written Full templates
Shapego generate writes cli/internal/recipes/full; aps embeds it; CI regenerates and fails on a diffaps new copies examples/full-single from a checkout or downloadA second template tree edited by hand
Source of truthGolden appGolden appTwo copies
Works offline, from a released binaryYesNo: needs a checkout or a network fetch (threat 4: no downloads at install)Yes
Drift detectionByte-for-byte golden test and the CI diffNone neededManual
Binary sizeAbout 0.5 MB of templatesNoneAbout 0.5 MB
Cost to reverseLowMediumHigh
Fits existing pathYes: the Minimal pipelineNoNo

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#

TopicDecision
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.lockbase-full for Full, base-minimal for Minimal, with every created file's hash
ContentExactly 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 emailSingle-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 printedaps 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 DockerA separate v0.2 item, done 2026-09-15: see ADR-0028's v0.2 implementation notes

Verification#

LevelCheck
TemplatesGolden test per preset: rendering with the placeholders reproduces every golden file byte for byte, and nothing else
GeneratorUnit tests for the go.mod derivation and the leak check
CLIaps 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
CIRegenerates 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)#

CheckResult
Templates128 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 checkoutaps 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 endNew 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 measuredCold caches and Docker image pulls, which depend on the network; measured with aps dev and Docker
Found while implementingA --local path containing a space produced an invalid go.mod for both presets; replace paths are now quoted when needed
esc
↑↓ move↵ openesc close