> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0480 — API-first release and vault-keeper agents

> Proposed · 2026-10-29

> [!NOTE]
>
> Extends [0257](https://docs.stackra.figentra.com/adr/0257-secret-manager-agent-and-the-secrets-inbox.md) (nobody reads a value) to
> generated machine secrets and preflights, and keeps
> [0267](https://docs.stackra.figentra.com/adr/0267-infisical-agent-vault-deferred.md) (Agent Vault deferred). Numbered after the
> highest ADR in flight (0479); the ADR steward reconciles the number.

## Context

The Academorix launch (2026-10-22 to 10-24) lost a day to store and secret work that was data, not code: no App Store
Connect API key with the Admin role, so the iOS build went through Transporter by hand; iPad and Play screenshot sets
re-exported from App Store Connect because the designed frames were not in the repository; the review demo account
half-seeded on production; privacy and Data safety answers typed from memory; and credentials pasted into chat. On
2026-10-29 the owner asked for launches without that manual work: a few keys created once, every other step done
through the platform APIs by agents, an owner approval only where a step is irreversible, and still nobody reading a
secret value.

## Decision

1. **A root kit, created once by the owner.** Four credentials, each delivered through `~/Secrets-inbox` and
   `pnpm secrets:import` into Infisical `prod:/ci/store` (except the Infisical identity, which is an id, not a
   secret): an App Store Connect API key with the Admin role (`ASC_API_KEY_ID`, `ASC_API_ISSUER_ID`,
   `ASC_API_PRIVATE_KEY`); a Google Play Console organisation account with a service account granted API access
   (`GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`); an EAS robot token (`EXPO_TOKEN`); and an Infisical machine identity that
   trusts only each product repository's `production` environment over GitHub OIDC and reads
   `/ci/store` (its id is passed to the workflow as a repository variable). Nothing else in a launch needs a new owner credential.
2. **Store presence is data in the product repository** ([0441](https://docs.stackra.figentra.com/adr/0441-product-repository-layout-assets-tree-and-root-runway-surfaces.md)
   decisions 1 to 3). The app's identity (bundle id, package, name per locale) and capabilities come from the root
   `runway.yaml` `mobile.<app>` entry; its listing from the `assets/store/<app>/<locale>/` folder that entry names
   (`name.txt`, `subtitle.txt`, `description.md`, `keywords.txt`, `privacy.yaml`, and optional `shortDescription.txt`,
   `promotionalText.txt`, `whatsNew.txt`); app-level facts from `assets/store/<app>/store.yaml` (the App Store Connect app id once the record exists, urls, review contact
   and notes, the demo account, seed and probe as secret NAMES, TestFlight groups, app groups). `privacy.yaml` holds
   one row per data type and source, naming a `@stackra/consent` category, and is the same in every locale. Screenshots
   are captured in CI from the app built from its template line (0441 decision 7) and never committed. `pnpm store
   build --root <product> --app <key>` checks all of it and generates every store screenshot size, the App Store App
   Privacy answers, the Play Data safety answers and a checklist into the gitignored `.stackra/store/<app>/`. An
   analytics or crash SDK in the product's or the template checkout's dependencies with no row naming it fails the
   build, so privacy answers travel with the feature. Core's `apps/mobile` is a reference app and has no store data.
3. **API first.** The releaser drives the App Store Connect API (bundle ids, capabilities, versions, metadata, review
   details with the demo account, screenshots, TestFlight groups and beta review, submit for review), the Google Play
   Developer API (one edit per run: listings, screenshots, tracks, validate, commit) and EAS (build and submit). The
   clients are `tools/scripts/store-asc.mjs` and `store-play.mjs`, with no SDK dependency: requests are signed with
   `node:crypto` (ES256 for Apple, RS256 for Google).
4. **Browser only where no API exists.** Creating the first App Store Connect app record, creating app group
   identifiers, the App Privacy form, creating the Play app and its first content-rating, target-audience and Data
   safety forms. These are done with the owner signed in, from the generated checklist and answers. No agent types a
   password, and no agent signs in.
5. **Dry run by default; real calls only from CI.** Every external call goes through `store-http.mjs`, whose default
   mode sends nothing and prints the planned calls; `--replay` serves recorded fixtures (the tests); `--live` and
   `--record` run only inside GitHub Actions. Nothing calls Apple, Google or Expo for real until the owner has created
   the root kit and approved the first live run.
6. **One owner approval per irreversible step.** `.github/workflows/runway-store.yml`, Runway's reusable `store`
   provider, runs each launch step from the product's main, building in Runway's checkout of the app's template line. A live run of any step runs in the product's `production` environment, which accepts only the protected main
   branch: the store and signing keys are production secrets, and production secrets live only there, never
   repository-wide (owner decision 2026-10-29). A dry run reads no secret and takes no environment. The CLI refuses a
   live `submit` (App Review), `promote` (Play production) or `seed` (a production write) outside `production`, and
   `promote` is the owner's own dispatch: no agent starts it (the promote-guard hook). Production deploys keep their own approval under
   [0170](https://docs.stackra.figentra.com/adr/0170-deployment-what-deploys-where-and-how-a-release-is-promoted.md).
7. **The review demo account is a release check.** `pnpm store review seed` calls the app's seed endpoint and `pnpm
   store review probe` signs in with the demo account on production and must see data before a submission.
8. **The vault-keeper agent works by name.** `pnpm vault preflight --env <e>` lists every secret a deploy or store
   release of `e` needs (the runway CI and member names, and for `prod` the root kit and the review secrets) as
   present, missing or placeholder, by NAME only, and fails on any gap. `pnpm vault generate NAME [--rotate]` creates
   or rotates a random machine secret written blind into Infisical through the secrets-import core; production needs
   the dry run's one-time token, bound to the operation, as in 0257. Third-party credentials are never generated
   here: they arrive through the inbox.
9. **Agent Vault stays deferred** (0267). Credentials reach the CI job as environment values over OIDC and never the
   model.

## Consequences

* A launch is: the owner creates the root kit once, does the few browser steps from the checklist, approves the
  submit in the `production` environment and dispatches the promote itself. Every other step is a workflow dispatch an agent can make.
* The iPad set, the 9:16 Play set, the privacy answers and the demo account can no longer be forgotten: `store build`
  and the probe fail first.
* Apple and Google change their APIs; the fixtures are in the documented response shapes until the first live run
  re-records them (`--record`), and a drift shows up as a replay failure, not on launch day.
* The App Store Connect key has the Admin role because app management needs it; it lives only in `prod:/ci/store`,
  readable only by jobs in the `production` environment.
* Data safety and App Privacy stay browser forms until the APIs accept them; the answers are generated, so the form is
  copying, not deciding.

## Enforcement

* `tools/scripts/__tests__/store.test.mjs`: the dry run sends nothing; live and record refuse outside CI; recorded
  fixtures for the App Store Connect and Play flows; `submit`, `promote` and `seed` refuse outside `production`; no
  credential or demo password reaches the output; `store build` checks consent categories, SDK rows, text limits and
  screenshot counts, and renders the exact store sizes.
* `tools/scripts/__tests__/vault-keeper.test.mjs`: preflight is names-only; generate and rotate write blind; the
  production token is bound to the operation.
* `.github/workflows/runway-store.yml`: a live run takes the `production` environment; its protection is applied by the
  owner (runbook), not checked by CI.
* `.agents/agents/releaser.md` and `.agents/agents/vault-keeper.md`: each agent is allowed only its own commands
  (`pnpm store`, the dispatch of the product's store workflow; `pnpm vault`).
* Planned — a `vault preflight` step in each deploy workflow (STA-1598).

## Supersedes and sources

`Supersedes: none`. Extends [0257](https://docs.stackra.figentra.com/adr/0257-secret-manager-agent-and-the-secrets-inbox.md); keeps
[0267](https://docs.stackra.figentra.com/adr/0267-infisical-agent-vault-deferred.md). Sources: owner request 2026-10-29; Academorix launch lessons
2026-10-24 (stackra notes); epic STA-1590, root kit STA-1591, which supersedes the manual store steps of STA-1555, STA-1557 and STA-1558.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.