# ADR-0047: 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 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:

```text
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](../roadmap.md#v12-client-templates).
