Full preset generation
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.Runhandles both presets:go generate ./internal/recipes/writesminimal/fromexamples/minimalandfull/fromexamples/full-single. Templates are never edited by hand.- Placeholders stay the two the Minimal preset uses:
example.com/acme-apibecomes the module path, thenacme-apibecomes the app name. go.modcomes from the goldengo.mod, for both presets: everyrequireis kept,apistock.devmodules take the CLI's library version, the goldenreplaceblock is dropped, and with--localareplaceis written for everyapistock.devmodule the golden app requires.go.sumisn't copied;aps newrunsgo 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.modremoves 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.
apsembeds 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 devchecks ports, the printed next steps explain the override. - Existing local volumes of
examples/full-singlewere created with the oldacmerole;docker compose down -vresets them.
Consequences#
aps new --preset=fullis 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-fullis recorded inapistock.lockand 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 |