All presents

IceVault: From Prototype to Production

Building a macOS menu bar app for cold backups — architecture decisions, Swift concurrency patterns, and shipping signed releases.

IceVault: From Prototype to Production

Building a macOS menu bar app for cold backups — architecture decisions, Swift concurrency patterns, and shipping signed releases.


Over the past few weeks, IceVault evolved from a "what if?" idea into a production-ready macOS app for automated backups to AWS S3 Glacier Deep Archive. Here's the story of the technical decisions, the architecture, and the path to shipping.

The Problem

Cloud backups are either expensive (real-time sync services) or cumbersome (manual upload workflows). Glacier Deep Archive is incredibly cheap ($0.00099/GB/month) but has a high friction barrier: AWS CLI, multipart uploads, credential management, and no native macOS integration.

I wanted something that sat in the menu bar, watched folders, and quietly archived files without thinking about it.

Architecture Decisions

Bounded Concurrency with Swift Structured Concurrency

The first challenge: uploading thousands of small files without saturating the network or melting the CPU. Swift's TaskGroup makes this elegant:

static func run<Input: Sendable, Output: Sendable>(
    inputs: [Input],
    maxConcurrentTasks: Int,
    operation: @escaping @Sendable (Input) async throws -> Output,
    onSuccess: @escaping (Output) async throws -> Void
) async throws

The BoundedTaskRunner schedules work with back-pressure — as tasks complete, new ones start. This prevents the "fork bomb" problem of unbounded concurrency while still saturating available bandwidth.

Actor-Based Progress Tracking

Upload progress is inherently stateful. Rather than fighting Swift's concurrency model, I leaned into it:

private actor UploadProgressTracker {
    private var completedRecordIDs: Set<Int64> = []
    private var inFlightBytesByRecordID: [Int64: Int64] = [:]
    private var completedBytes: Int64 = 0
    
    func update(recordID: Int64, uploadedBytes: Int64) -> UploadProgressSnapshot { ... }
}

The actor isolates mutable state. The UI observes snapshots. No data races, no locks, no @MainActor gymnastics.

Multipart Uploads for Large Files

Files over 100MB get chunked into parts and uploaded via S3's multipart API. This serves two purposes:

  1. Resumability: If a 5GB upload fails at 4.9GB, you don't start over
  2. Parallelization: Multiple parts upload concurrently

There's also stale upload cleanup — abandoned multipart uploads older than 24 hours get purged automatically.

The Production Pipeline

Code Signing and Notarization

A menu bar app that uploads files to the cloud triggers every Gatekeeper alarm. To ship a trustworthy app:

  • Developer ID signing: App bundles signed with Apple-issued certificates
  • Notarization: Apple scans and approves each release
  • Stapling: The notarization ticket is embedded in the DMG

The result: users double-click the DMG, drag to Applications, and the app opens without scary security warnings.

GitHub Actions Release Flow

Tagging v* triggers the pipeline:

  1. Build arm64 and x86_64 binaries on separate runners
  2. Sign both app bundles
  3. Notarize and staple
  4. Package into DMGs
  5. Publish to GitHub Releases
  6. Update the Homebrew tap

The entire release process is automated. Push a tag, get signed binaries.

Homebrew Distribution

brew install lydakis/tap/icevault

The Homebrew tap auto-updates on each release. Users get updates via brew upgrade.

What's Working

  • Incremental sync: SQLite inventory tracks what's already uploaded
  • Credential resolution: Keychain → ~/.aws/credentials → environment variables
  • Scheduled backups: LaunchAgent support for daily/weekly automation
  • Resume-safe: Cancel and resume backups without losing progress
  • Remote validation: Compares local checksums with S3 to detect corruption

What's Next

  • Restore UI (currently headless only)
  • Exclude patterns (.git, node_modules)
  • Bandwidth limiting
  • Versioned backups (keep N versions of each file)

The Economics

Back up 1TB to Glacier Deep Archive: ~$1/month. Compare to Dropbox ($120/year for 2TB) or Backblaze ($70/year unlimited). The tradeoff is retrieval time (12-48 hours) and egress fees — but for true cold storage, it's unbeatable.


IceVault is open source at github.com/lydakis/icevault. If you're the kind of person who has terabytes of photos, videos, or archives that you rarely touch but never want to lose, it might be for you.