apistockdocs
v0.4 GitHub apistock.dev
Decisions/Records

Client templates

ADR-0047Proposed (2026-09-15)

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 new runs only go mod tidy and git init.
  • ADR-0021 and ADR-0016: output is recorded in apistock.lock so it can be upgraded; the same aps version 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#

  1. Keep client templates in this repository, embedded in aps like the Go recipes.
  2. Clone template repositories from GitHub at aps new time and search-and-replace the app name.
  3. 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:

output
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 aps release embeds a manifest of compatible template versions and each archive's SHA-256. aps never fetches a branch or "latest".
  • aps downloads the archive, checks the hash (and the Sigstore signature, as for aps releases), and caches it in the user cache directory (~/.cache/apistock on 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 with os.Root; it never runs scripts from the archive (no postinstall, no Gradle or Xcode build during aps 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.ts reads a generated apistock.json.
  • iOS: name and bundle ID through .xcconfig, or a project generated by XcodeGen or Tuist. project.pbxproj is never edited by search and replace.
  • Android: applicationId in build.gradle.kts; moving the Kotlin package is a declared rename.
  • Docs and dashboard: .env and 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#

  1. The fetch, verify and fill-in mechanism, proved with template-docs (lowest risk).
  2. template-dashboard.
  3. template-expo.
  4. template-ios and template-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 aps version 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 new gains its first network access beyond the Go module proxy. --templates and the cache keep offline use possible.
  • The threat model grows: template repository compromise, archive tampering, and dependencies in npm, CocoaPods and Gradle that govulncheck doesn'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 fetchTemplate operation; 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 new with it against examples/full-single and builds the result (npm run build, eas build --local or xcodebuild, gradle assembleDebug).
  • Roadmap milestone: v1.2 Client templates.
esc
↑↓ move↵ openesc close