14. Transcripts live in S3 only — CouchDB attachment support removed
Date: 2026-06-08
Status
Accepted (supersedes ADR 0013 and the remaining attachment-read parts of ADR 0011)
Narrowed by ADR 0027: this ADR is about where transcript bytes durably live, and that hasn't changed — S3, never a CouchDB attachment. But CouchDB is no longer blind to transcript content: full-content chunk docs carry each turn's parsed text, and the reader now serves GET /api/sessions/{id}/transcript from those chunks by default, falling back to the S3 blob only when it reaches further. So "S3 only" remains true of the durable, byte-exact copy and is no longer true of the read path. See "Consequences" below.
Context
ADR 0013 made S3 the transcript's durable home and demoted the CouchDB transcript attachment to an opt-in, gated on the couchTranscriptAttachment feature flag (default false), "for deployments without an S3 backend." The webapi still read transcripts S3-first with a fall back to a legacy CouchDB attachment.
In practice the flag was never enabled, S3 is part of the standard stack (ADR 0008), and the dual code paths carried real cost: an opt-in write branch in the hook and the on-disk backfill (backfill), a raw-HTTP attachment reader in the webapi, and hasTranscript / transcriptSize logic that had to consult _attachments or transcript_bytes. The "no S3" escape hatch isn't worth a permanent second storage path — a deployment without an object store can run any S3-compatible server (Garage, MinIO, R2, AWS) by setting env.
The 281 legacy summary:<id> docs that still carried a transcript.jsonl attachment were verified recoverable from S3 (every attachment byte-identical to, or a byte-exact prefix of, the Garage copy), then removed and the DB compacted (367 MB → 17 MB) before this change.
Decision
Remove CouchDB transcript-attachment support entirely:
- Hook /
backfillno longer write a transcript attachment under any condition. ThecouchTranscriptAttachmentfeature flag and theputAttachmentCouchDB helper are deleted. - S3 is the transcript's sole durable home. The hook uploads
transcript.jsonl(andsummary.json) only to the S3 blob store. - The webapi reads transcripts from S3 only. The raw-HTTP attachment reader (
readCouchAttachment) is gone; there is no CouchDB fallback. hasTranscript/transcriptSizederive solely from the summary doc'stranscript_bytes.
Consequences
- CouchDB holds no transcript bytes — no attachments, ever. The primary store stays compact; replication and view builds are cheaper. (Since ADR 0027, chunk docs do hold parsed per-turn text. That's a projection for querying, not the byte-faithful transcript, and it's what makes the store searchable and readable mid-session.)
- A deployment with no S3 backend still loses the byte-exact transcript — but no longer loses transcript content outright, since content chunks live in CouchDB. S3 remains required for a faithful copy (and for export).
hasTranscriptno longer depends ontranscript_bytesalone: it is true when either the summary records bytes or the session's chunks carry turns. Without that, a running or crashed session showed metadata and no transcript even though its content was already in CouchDB. Legacy attachment-only docs were backfilled withtranscript_bytes(from their S3 object size) as part of the attachment removal, so they still surface in the UI.- The Bun-specific
nano.attachment.getfinding from ADR 0011 no longer applies to this codebase (no attachment is read); the ADR is retained only as history.