01 — Architecture
Decision
Use a Swift native filesystem engine, a SwiftUI Mac app on macOS 27 Golden Gate, SQLite local catalog, and a Next.js documentation site. The hosted website is not the application. See ADR-004.
Why Swift
Burrow™ is macOS-specific. The engine will eventually need first-class access to:
- mounted volume metadata;
- file resource values and extended attributes;
- macOS permissions and sandbox/security-scoped resources;
- iCloud file metadata;
- file coordination;
- Trash operations;
- filesystem events;
- removable media lifecycle;
- native notifications/background behavior.
Rust remains a good option for portable high-performance hashing libraries, but a separate Rust service is unnecessary complexity initially. Native or packaged BLAKE3 support can be introduced behind a hashing interface.
Logical Architecture
┌───────────────────────────────────────────────┐
│ SwiftUI Mac app (macOS 27 Golden Gate) │
│ Dashboard / Storage / Files / Settings / … │
└──────────────────────┬────────────────────────┘
│ in-process BurrowEngine
┌──────────────────────▼────────────────────────┐
│ Burrow Engine │
│ │
│ Volume Discovery Scanner Cloud Classifier │
│ Hashing Rules Backup/Archive(*) │
│ Safety Gate Activity/Audit │
└──────────────┬───────────────────┬────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────────┐
│ SQLite │ │ macOS Filesystem │
│ Local State │ │ iCloud/OneDrive │
└─────────────┘ │ External Volumes │
└───────────────────┘
(*) Disabled in v0.1
Process Boundary
The Next.js website publishes documentation only. The Mac app UI must never be granted unrestricted filesystem primitives. It requests domain operations such as:
listVolumesstartScangetScanStatusqueryFiles(0.1: typed fixture catalog query in the Next.js UI; later catalog bind. Rejects unapproved path/root requests. No HTTP files route.)getDuplicateGroups
Future mutation operations must pass through a centralized Safety Gate.
Engine Modules
VolumeDiscovery
Detects mounted volumes and stable identities.
ScanCoordinator
Creates scan runs, manages cancellation/progress, delegates traversal.
FileScanner
Enumerates selected roots and captures metadata.
StorageClassifier
Classifies records as local, iCloud, OneDrive, removable, etc. Provider location (INTERNAL_VOLUME, EXTERNAL_VOLUME, ICLOUD_DRIVE, ONEDRIVE, OTHER_LOCAL) is separate from allocated bytes and from backup protection. VolumeClassification still maps volume flags only; StorageClassifier adds provider types and 0.1 protection (LOCAL_ONLY / SYNCED_ONLY / UNKNOWN). StorageClassifier.protectionState(forProviderType:) is the public, unit-testable 0.1 derivation; it reuses StorageProtectionState and never emits later-release backup states. See docs/02-STORAGE-MODEL.md.
HashService
Release 0.2. HashService writes BLAKE3 digests to file_hash. The cache key is filesystem identity, logical size, modified time, and cloud state, not the file name. Cloud-only files stay DEFERRED and are not opened. Symlinks are not followed. Content reads are capped by maxConcurrentReads (default 2), separate from the scanner. A partial signature for large files is not exact-duplicate proof. Migration 5 creates file_hash only. It does not rebuild or delete file_item.
DuplicateAnalyzer
Release 0.2. DuplicateAnalyzer.evaluate(in:) groups files that share a logical size and a current full BLAKE3 digest. Directories, symlinks, zero-byte files, and partial signatures are not exact groups. Each member keeps its path, provider, and volume. Reclaimable space is the local allocated bytes beyond the single largest local copy. Cloud logical size is not reclaimable, and the engine does not choose a file to delete. Migration 6 creates duplicate_group and duplicate_member only. Replacing a previous exact group deletes those analysis rows and does not rebuild or delete file_item.
RecommendationEngine
Release 0.2. RecommendationEngine.evaluate(in:) writes informational rows for exact duplicates, large stale files, archive candidates, downloads or installers, and local bytes on a cloud copy of an exact group. Each row has a reason code and a why. None say "safe to delete." RecommendationExecution.perform always throws and does not change files. Migration 7 creates recommendation only. Replacing those rows does not rebuild or delete file_item. Archive and backup suggestions stay informational.
SafetyGate
Central authorization point for all future filesystem mutations.
BackupEngine / ArchiveEngine
Designed now, implemented after v0.2.
ActivityService
Records scans, analysis, errors, and eventually all mutations. In 0.1 the persist/query surface is in-memory: record / query(scanId:). Completing or cancelling a fixture walk (ScanErrorWalker) writes scan-started, scan-completed or scan-cancelled, and scan-error rows. Mutation rows are denied SafetyGate attempts only. The documented catalog activity table is not migrated yet; do not invent a second activity table. A bounded run.log beside the catalog is the owner-pass transcript. It is not that table and it does not store one line per file. Hosted HTTPS cannot call 127.0.0.1, so the public Activity view renders fixture events.
UI/Engine Communication
The v0.1 transport is HTTP on IPv4 loopback:
- bind
127.0.0.1only (never a wildcard interface); - default port
8741, or0for an ephemeral port; - every
/healthand/identityrequest requires headerX-Burrow-Secret(legacyX-PlatypusVault-Secretis still accepted); - the secret is random per process unless
BURROW_ENGINE_SECRET, legacyPLATYPUSVAULT_ENGINE_SECRET, or--secretis set; - typed JSON contracts live in
/contracts; - no arbitrary path, SQL, or shell endpoints.
The Next.js client calls http://127.0.0.1 only. Hosted HTTPS deployments (burrow.jasonkoch.io / burrow-dev.jasonkoch.io) cannot reach the engine and must display disconnected. Mixed-content rules block https pages from calling loopback HTTP.
A native wrapper/IPC approach may replace HTTP later without changing domain contracts. See contracts/README.md.
SQLite
SQLite is authoritative for application state but never authoritative for whether a file currently exists. Filesystem reality wins and is reconciled through scans.
Enable foreign keys and WAL mode where appropriate.
The 0.1 catalog file lives under Application Support (Burrow/catalog.sqlite) or a temporary directory in tests. A legacy PlatypusVault/catalog.sqlite is reused when the Burrow™ catalog is missing. It must never be created in Documents, iCloud Drive, or OneDrive. Ordered migrations create volume, provider, storage_root, scan, scan_root, scan_error, and file_item, plus an index on file_item(volume_id, current_path). A scan writes sizes in batches of 2000 instead of one transaction per file, with memory mapping off, an 8 MiB page cache, a 32 MiB SQLite soft heap limit, and a truncated write-ahead log after each batch. The on-disk catalog still grows with the inventory. The walk streams directory entries and does not retain every path. Catalog writes are metadata-only; they do not copy, move, or delete user files. Migration 5 adds file_hash without deleting or rebuilding existing rows. Migration 6 adds duplicate_group and duplicate_member the same way.
Concurrency
Scanning should be cancellable and bounded. Do not open/hash an unbounded number of files concurrently.
Metadata enumeration and hashing should use separate concurrency controls because hashing is I/O intensive.
Incremental Scanning
The catalog should allow unchanged files to reuse metadata/hash results. Future filesystem-event monitoring may mark roots dirty, but full correctness must never depend solely on events.
Error Philosophy
A permission error or unavailable cloud placeholder must not fail an entire scan. Record the issue, continue where safe, and expose incomplete coverage in the UI. Engine query: Catalog.scanCoverage(scanId:) returns scan status, error_count, incomplete roots, and scan_error rows. ScanErrorWalker defaults to disposable fixture roots. ScanCoordinator.startScan inventories catalog-approved roots, reports each file as it is read, and does not mutate user files. No HTTP scan-error route exists yet.
Storage analytics (VLT-32) is Catalog.storageAnalytics() — a catalog-read report over the latest scan. It does not add HTTP routes. Dashboard cards and Storage drill-down (VLT-34 / VLT-36) consume that report.
Packaging Direction
The end product should feel like a Mac utility, even though the management UI is Next.js. Packaging options should be evaluated after v0.1 proves the engine/UI boundary.
Architectural Non-Goals
- cross-platform support in the first release;
- remote web access to the management UI or engine;
- multi-user server architecture;
- uploading inventory to a SaaS backend;
- embedding arbitrary shell execution in the UI.
Public documentation and the management UI are the Next.js site at burrow.jasonkoch.io. That hosted console is not remote access to the engine. See docs/hosting.md.