Skip to content

ADR-0027: Transaction attachments stored on-device in app files

  • Status: Accepted
  • Date: 2026-08-16
  • Deciders: maintainers
  • Related: ADR-0010, ADR-0015

Context

Users want to attach files to a transaction (txt, images, pdf, csv, xlsx, docx). The app is offline-first (ADR-0015) and its security posture is "everything stays on device" with an encrypted DB (ADR-0010).

Decision

  • Bytes live in the app-private filesDir/attachments directory under a UUID filename; the encrypted Room DB stores metadata only in a new attachments table (schema v8, MIGRATION_7_8): id, transactionId (FK CASCADE), fileName, mimeType, sizeBytes, type (enum dbValue), storedPath, createdAt.
  • At-rest protection relies on the OS file-based encryption of app-private storage plus the existing allowBackup=false / data_extraction_rules guards (no cloud copy path).
  • AttachmentRepository owns the file lifecycle — FK CASCADE drops rows but not files, so the repository deletes matching files when an attachment or a transaction is removed.
  • Validation is pure and unit-tested: MIME/extension whitelist (txt, image, pdf, csv, xlsx, docx) + a 10 MB per-file cap, mapped to typed StageResult (Ok | UnsupportedType | TooLarge | ReadFailed) for Snackbar feedback.
  • Draft-then-save: attachments are staged (file copied, no row yet) as the user adds them and persisted atomically with the transaction save via replaceAttachments. New (manual/unmatched) transactions can carry attachments because files stage before the transaction id exists.
  • Not exported in CSV/JSON (v1) — ExportCodec is text-only and the binary bytes stay out of the portable export.

Consequences

  • Encrypted-at-rest posture is preserved without duplicating a second crypto key; deleting a transaction removes its attachments.
  • filesDir grows with attachment bytes; the 10 MB cap bounds a single file.
  • Attachments are not portable via export/import (documented limitation).

Rejected alternatives

  • BLOB bytes in the SQLCipher DB — keeps encryption explicit and simplifies file lifecycle, but bloats the DB and pulls bytes through Room flows; files-on-disk keeps the DB light.
  • SAF persistable URIs — not durable across uninstall/provider changes.
  • Base64 in JSON export — export bloat and portability mismatch for binary.