Client templates
Status: Proposed (2026-09-15) · Would amend: ADR-0010, ADR-0014, ADR-0021, ADR-0029
Context#
apistock generates the Go API. Most products built on it also need clients: a documentation site for the app (for example docs.example.com), a dashboard, and mobile apps. Builders today write these by hand, then wire sign-in, the API client and configuration themselves. The maintainer wants aps new to offer them: "do you need a dashboard? mobile? which kind?", then create each client with the app name and every choice already filled in.
Constraints from earlier decisions:
- ADR-0021 and threat model rows 4–6 (ADR-0029): no generator step downloads files or runs code;
aps newruns onlygo mod tidyandgit init. - ADR-0021 and ADR-0016: output is recorded in
apistock.lockso it can be upgraded; the sameapsversion must produce the same app. - ADR-0010: an admin UI is a post-v1 generated module; architecture lists a mobile app as a non-goal.
- Clients use other toolchains (Node, Xcode, Gradle) with their own CI and release cadence.
Options#
- Keep client templates in this repository, embedded in
apslike the Go recipes. - Clone template repositories from GitHub at
aps newtime and search-and-replace the app name. - Separate template repositories, released as pinned, verified archives, filled in from a manifest each template declares.
Decision#
Option 3.
Repositories#
The Go library, aps and examples/ stay in this repository. Each client template has its own repository, CI and releases:
| Template | Stack | Covers |
|---|---|---|
template-docs |
Static docs site fed by the app's api/openapi.json |
The app's public docs subdomain |
template-dashboard |
Web app (stack decided when the template is built) | Sign-in, 2FA, passkeys, account, an example resource |
template-expo |
Expo (React Native) | iOS and Android from one codebase |
template-ios, template-android |
SwiftUI, Kotlin with Jetpack Compose | Native apps, only after Expo and on demand |
The generated project is one folder the builder owns:
shop/
api/ the Go app
docs/ from template-docs
dashboard/ from template-dashboard
mobile/ from template-expo, or ios/ and android/
Fetching: pinned, verified, cached#
- Each template release publishes an archive (
template-expo-v1.2.0.tar.gz). - Each
apsrelease embeds a manifest of compatible template versions and each archive's SHA-256.apsnever fetches a branch or "latest". apsdownloads the archive, checks the hash (and the Sigstore signature, as forapsreleases), and caches it in the user cache directory (~/.cache/apistockon Linux).--templates <path>uses local checkouts instead: for template development, offline use and CI.- A download is a new generator operation,
fetchTemplate, limited to the manifest's URLs and hashes. It extracts into the project withos.Root; it never runs scripts from the archive (nopostinstall, no Gradle or Xcode build duringaps new).
Filling in: declared, not search and replace#
Each template ships apistock-template.json:
| Field | Purpose |
|---|---|
variables |
Values it accepts, with validators: app name, display name, bundle or application ID, API URL, enabled sign-in methods |
render |
Files rendered with text/template |
rename |
Paths that depend on a variable (the Android package directory) |
api |
The API contract it needs, for example auth/v1 |
Choices go into as few files as possible, like "one wiring file per feature":
- Expo:
app.config.tsreads a generatedapistock.json. - iOS: name and bundle ID through
.xcconfig, or a project generated by XcodeGen or Tuist.project.pbxprojis never edited by search and replace. - Android:
applicationIdinbuild.gradle.kts; moving the Kotlin package is a declaredrename. - Docs and dashboard:
.envand one config file.
API contract#
Clients call the API through clients generated from the app's api/openapi.json (TypeScript for docs, dashboard and Expo; Swift and Kotlin generators for native), never hand-written endpoint lists. A template declares the API contract versions it supports; aps new refuses a combination the manifest doesn't list.
Prompts#
| Prompt | Flag | Default |
|---|---|---|
| Docs site | --docs |
no |
| Dashboard | --dashboard |
no |
| Mobile (none, Expo, native iOS and Android) | --mobile none|expo|native |
none |
| Bundle or application ID (asked only with mobile) | --bundle-id com.example.shop |
derived from the module path |
"Hybrid" (Capacitor-style) apps aren't offered: they overlap with Expo, and every choice is a template to keep working.
Ownership and upgrades#
Client output is one-shot at first, like aps gen resource: the builder owns it and aps upgrade doesn't merge it. apistock.lock records each template, its version and file hashes, so upgrades can be added later without guessing what was generated.
Order#
- The fetch, verify and fill-in mechanism, proved with
template-docs(lowest risk). template-dashboard.template-expo.template-iosandtemplate-android, only when builders ask for native apps.
Why#
- Separate repositories match separate toolchains, CI and contributors, and keep this repository's Go checks fast.
- Pinned, hashed archives keep the promise that one
apsversion produces one app, and keep a compromised template repository from reaching new apps. - A declared manifest makes filling in reviewable and testable; search and replace breaks Xcode projects and Android packages.
- Generated API clients keep clients in step with the Go app's endpoints.
Trade-offs#
- Four or five template repositories to release and keep compatible; each mobile stack adds its own sign-in, passkey and CI work. Native iOS and Android triple the mobile cost, so they come last.
aps newgains its first network access beyond the Go module proxy.--templatesand the cache keep offline use possible.- The threat model grows: template repository compromise, archive tampering, and dependencies in npm, CocoaPods and Gradle that
govulncheckdoesn't cover. - Client templates can't be byte-for-byte golden-tested against this repository's examples; CI must build them instead.
Consequences#
- When accepted: ADR-0021 gains the
fetchTemplateoperation; ADR-0029 gains rows for template supply chain; ADR-0014's prompt table gains the rows above; ADR-0010's admin UI becomes the dashboard template instead of a module inside generated apps; architecture's non-goals drop "a mobile app". - CI for each template runs
aps newwith it againstexamples/full-singleand builds the result (npm run build,eas build --localorxcodebuild,gradle assembleDebug). - Roadmap milestone: v1.2 Client templates.