> ## Documentation Index
> Fetch the complete documentation index at: https://sponson.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0003: Receipts live on an orphan git branch, behind a ReceiptStore interface

> Architecture decision record 0003. Status: Accepted. Amended by 0016: each environment and scope now has its own branch,

Date: 2026-10-10

## Status

Accepted. Amended by [0016](/contributing/adr/0016-one-receipts-ref-per-scope): each environment and scope now has its own branch,
`sponson-receipts/<env>/<scope>`; `sponson/receipts` is read only as a fallback for scopes not yet migrated.

## Context

Drift detection, destroy and resuming a partial apply all need to know what the previous run did. The recommended
place to run `apply` is CI, where every job starts on a clean machine, so a receipt kept in the working directory is
gone by the next run.

Options considered:

| Option | Why not |
| - | - |
| commit receipts to the main branch | commit noise and merge conflicts; a bot commit on a pull request branch triggers the workflow again |
| git notes | must be fetched explicitly, invisible in the GitHub UI, poorly supported by tooling |
| tags or comments on the resources themselves | not every provider has a place for them (a Clerk redirect URL does not) |
| pull request comments | production has no pull request; parsing comments is fragile |
| CI caches or artifacts | they expire and get evicted; they are not storage |

## Decision

Receipts are written through an interface, `ReceiptStore` (`packages/core/src/types.ts`): `read`, `write`, `list`,
and the lock operations `acquireLock`, `renewLock`, `readLock`, `releaseLock`. Two implementations ship:

* `git-branch` (default): an orphan branch `sponson/receipts` in the user's repository, on the `origin` remote.
  CI already has a token that can push to it (`contents: write`), so no new credential is needed.
* `local`: `.sponson/receipts/`, used when there is no remote and by tests.

Both use one layout (`packages/core/src/receipts/layout.ts`): `<environment>/<scope>/latest.json`, one
`<runId>.json` per run, and `lock.json`. Each run adds a file, so concurrent scopes never conflict on content, only on
the ref. The git store treats a rejected non-fast-forward push as a failed compare-and-swap: fetch, re-check, retry
with jittered backoff within a time budget (`SPONSON_STORE_BUDGET_MS`, default 60 s). When the budget runs out it
reports `STORE_CONTENDED`; when the server keeps refusing the push it keeps a copy under `.sponson/unpushed/` and
reports `STORE_REJECTED`.

## Consequences

* A CI run sees what the previous run did without any service outside the repository.
* The receipts branch history is a record of every run. Because receipts hold only keyed hashes and non-sensitive
  outputs ([ADR 0009](/contributing/adr/0009-single-point-redaction)), the branch is safe to keep.
* A hosted store is a third implementation of the same interface; nothing in the engine changes.
* Every store operation is a fetch and a push, which is slow under contention. The lease's renewal paces itself so a
  slow store cannot queue renewals ([ADR 0008](/contributing/adr/0008-scopes-succession-and-leases)).
* Deleting the branch makes Sponson forget what it manages: `plan` then reports Sponson's resources as unmanaged
  rather than failing.


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