# spindle-cache — an actions/cache for tangled CI, blobs on your own PDS > handoff spec, written 2026-08-08 from the stream operations session > (tangled.org/zat.dev/stream — see its docs/deploying.md and > .tangled/workflows/ for the motivating workloads). This is its own > project; it landed here because zds is the PDS whose blob store makes > the design honest. Whoever picks this up: everything below is > design-complete enough to start, and nothing below is sacred. ## the problem Tangled spindle CI has no artifact/workspace cache. Every push pays the full cost of anything the workflow computes: for zig projects that means the ReleaseSafe compile (~10 min for stream) on every single run. GitHub solved this with actions/cache years ago; spindle has only: - Nixery layer caching (nixery engine) — dependency *images*, not build outputs - Nix binary caches (microvm engine) — nix *store paths*, not your zig-cache There is no key-addressed "save this directory, restore it next run". ## the insight Two existing mechanisms compose into the whole feature: 1. **Spindle's plugin system is the nix registry.** Any workflow can pull a CLI from any flake ref: ```yaml dependencies: git+https://tangled.org/@zat.dev/spindle-cache: - spindle-cache ``` Publishing a repo with a flake IS publishing an action. 2. **The PDS is the storage, and eviction falls out of atproto's data model.** The spindle docs' own release recipe already uploads blobs via `com.atproto.repo.uploadBlob` and pins them with `sh.tangled.repo.artifact` records. A cache entry is the same shape: a record keyed by cache key, referencing tarball blob(s). PDS blob GC deletes unreferenced blobs — so **overwriting a key's record automatically releases the old blobs**. The eviction policy nobody wants to write is free. ## UX (mirror actions/cache, people know it) ```yaml steps: - name: restore build cache command: spindle-cache restore --key "zig-$(zig version)-$(sha256sum build.zig.zon | cut -c1-16)" --path .zig-cache - name: build command: zig build test - name: save build cache command: spindle-cache save --key "..." --path .zig-cache ``` - `restore`: resolve the record (getRecord by rkey = sanitized key) on the configured PDS, fetch blob(s), unpack to --path. Miss = exit 0, empty dir. Optional `--restore-keys` prefix fallback like actions/cache. - `save`: tar + zstd the path, `uploadBlob` (chunked, see limits), then `putRecord` with the blob refs. Last writer wins; races are harmless for caches. - Auth: `ATP_IDENTIFIER` + `ATP_APP_PASSWORD` pipeline secrets — the exact pattern the spindle artifact-publishing recipe documents. ## the lexicon (strawman) ``` dev.zat.spindle.cache (or whatever namespace fits) key: string (the full cache key, also sanitized into rkey) createdAt: datetime toolVersion:string chunks: blob[] (ordered zstd tarball chunks) totalBytes: integer pathHint: string (informational: what was cached) ``` rkey = a hash of the key so arbitrary key strings stay valid. One record per key; putRecord replaces → old chunks unreference → PDS GC collects. ## constraints, named honestly - **Blob size caps.** Hosted PDSes cap blobs (tens of MB typical). The CLI must chunk (e.g. 32 MiB chunks, blob[] in the record). On zds the cap is a flag (`blob_upload_limit`) — running your own PDS makes big caches a config decision, which is exactly the dogfooding angle: zds is the reference deployment for this tool. - **Caches are public.** PDS blobs are world-readable. Fine (good, even) for OSS build caches; disqualifying for anything containing secrets. README must say so loudly. - **Bandwidth.** A ~300 MB zig-cache round-trips through the PDS each run. Worth it against a 10-min compile; measure and document the break-even. Chunked parallel fetch helps. - **PDS blob GC timing** varies by implementation — verify zds actually collects unreferenced blobs (if not, that's a zds issue this project smokes out — also valuable). - **Repo-level trust**: anyone can publish records claiming any key; the consumer only ever reads from ITS OWN configured identity's repo, so there is no cross-account cache-poisoning surface as long as the CLI never resolves other identities. ## implementation notes - Language: go or zig, single static binary, flake packaging it (see tangled's own repos for flake conventions). go is faster to ship; zig keeps the toolchain story consistent with the rest of zat.dev. - ~600 lines: tar/zstd, chunked uploadBlob with retry, putRecord/ getRecord, key sanitization. No daemon, no state. - First adopter: stream's `.tangled/workflows/ci.yml` (restore/save around `zig build test -Doptimize=ReleaseSafe`) and later its admission workflow. Expected effect: ReleaseSafe test step drops from ~10 min to incremental-compile time on warm keys. - Name ideas: spindle-cache (obvious), stash, hoard. ## why this is ecosystem-forward Every tangled CI user has this problem and none of them has a cache. This ships as: a repo + a flake + a lexicon, storage the user already owns, zero new infrastructure, and the first real demonstration that "actions marketplace" on tangled is just flakes + PDS records. The feature request we almost filed becomes a thing the ecosystem gets to adopt instead.