cairn — Immich sync
Sync photo deletions to Immich
Only for iPhone
Free · Designed for iPhone. Not verified for macOS.
When you delete a photo on your iPhone, cairn moves the matching photo on your Immich server to Trash. Every run is a preview — nothing moves until you confirm.
cairn reconciles your iPhone photo library against your own Immich server. When you delete a photo on your phone, cairn moves the matching photo on Immich to Trash.
That's the whole job. cairn doesn't upload photos, show albums, edit metadata, or run AI — the Immich app already does those things. cairn closes the one loop the Immich app doesn't: photos that live on the server after you've deleted them from your phone.
HOW IT WORKS
— Content identity is SHA1 (the same identifier Immich uses server-side).
— cairn subscribes to iOS's deletion log, so it sees soft-deletes as you make them.
— A configurable quarantine window holds confirmed deletions before anything moves, so an accidental mass-offload has time to be caught.
— Every run shows you the candidate list first. Nothing happens on the server until you confirm.
SAFETY MODEL
— Trash, not delete. cairn moves assets into your Immich Trash — Immich retains them for 30 days, and restore is one tap.
— Percent cap + floor. If a single run would move more than a threshold of your matched photos to Trash, it aborts without touching the server.
— Breadcrumbs. Every run is tagged on Immich (cairn/v1/run/[id]) so you can find it server-side.
— Forensic journal. Local append-only log records every step — planned, tagged, trashed, restored, failed.
— Exclusions. Protect specific photos from ever being flagged.
— Indexing scope. Restrict cairn to a specific set of Photos albums. Photos outside the scope are silently ignored — never hashed, never proposed for trash. Useful if you want to manage just one album and leave synced family albums alone.
PRIVACY
— No analytics, no telemetry, no crash reporting, no ads.
— No cairn backend. Your iPhone talks directly to your Immich server using your API key.
— Credentials stay in the iOS Keychain. Nothing leaves your device besides requests you configured.
REQUIREMENTS
— An Immich server you run or control.
— An Immich API key. cairn requests these scopes: asset.read, asset.view, asset.download, asset.delete, tag.create, tag.asset, tag.read. The Setup screen lists them when you paste your key.
cairn is not affiliated with the Immich project. It talks to Immich over its public API; compatibility only.
Source is MIT-licensed at github.com/glarue/cairn.
more Fixes trash requests that could get stuck permanently.
If a trash run was interrupted partway through — server unreachable, connection dropped mid-sync, VPN down — the queued retry could wedge and report "A tag with that name already exists" on every attempt after that, and no amount of retrying could clear it.
Every trash run writes a breadcrumb tag to Immich so the run can be reconstructed later, and cairn was creating those tags with an endpoint that rejects duplicates rather than the one that reuses an existing tag. Because a retry reuses its original run's tag, every attempt after the first was refused.
Retries now reuse the run's existing tag instead of failing on it, and any request already stuck this way clears itself on the next sync after you update. Run breadcrumbs also nest properly under cairn / v1 / run in Immich's tag list now, instead of appearing as one flat entry.
0.4.4 Aug 20
cairn can now move deletions to Immich's Trash automatically, without a manual "Trash all" tap. This is opt-in and gated by your safety limits.
Because no earlier version ever deleted on its own, this update resets everyone to Cautious (manual) once, so no one is switched into automatic deletion silently. Open Settings → Safety & limits → Deletion strictness to choose:
- Cautious (the new default): nothing is trashed automatically. Deletions wait as "Ready to trash" for you to confirm.
- Balanced: deletions are trashed automatically once they clear the quarantine window.
- Autonomous: deletions are trashed automatically, with no quarantine wait.
Either automatic mode still respects your percent and count safety limits: a run that would exceed them — or a first sync — is left for you to review instead. Everything goes to Immich's Trash, which keeps a 30-day recovery window, and automatic trashing runs on background refresh too.
Pending Review is tidier for bulk deletes. When several photos are queued to expire on the same day, they now stack into one "N photos" card instead of a long run of rows. Tap to expand and review them one by one; the trash, exclude, and dismiss actions on the card apply to the whole batch.
Also fixed: trashing the "Ready to trash" list now updates the quarantine banner immediately instead of waiting for the next sync, and the countdown on the Status card matches the one shown inside Pending Review.
0.4.3 Jul 16
Pending Review is steadier and clearer. The quarantine queue now appears the moment you open the app, instead of looking empty until the next sync finishes. Labels are plainer, too: the held section is "Queued," and each photo shows when it will be trashed ("Trashes in 5 days," then "Ready to trash") instead of the vaguer "eligible."
The deletion-strictness levels have clearer names. They now read as a scale, from most to least cautious — Cautious, Balanced, and Autonomous (previously Strict, Trusting, and Auto). Behavior is unchanged and your current setting is preserved.
Connection settings are reorganized around your server and key:
- Tap "API key" for one place to view, copy, replace, or disconnect. "Replace key" swaps in a new key for the same Immich account without re-onboarding — your indexed state, journal, and history are kept.
- The sign-in screen now remembers addresses you've used.
- Your Immich server version is shown, with a gentle heads-up if it's newer than the version of cairn it was tested against.
Plus smaller fixes: consistent slider styling and a tidied-up Status card.
0.4.2 Jun 23
Large photo libraries now finish the first scan. On libraries of 100,000+ photos, the initial scan could stall with no visible progress — a metadata pass was blocking the main thread. It now runs in the background, so the scan keeps moving and the app stays responsive. Thanks to Ryan Lim (@ryanlim) for diagnosing this and contributing the fix.
More reliable sync when you reopen the app. Returning after the app has been idle now runs a single catch-up sync, and only when one is actually due — instead of occasionally spinning the sync icon without syncing, or leaving a half-finished run in "Last sync details." Backgrounding mid-sync now ends the run cleanly rather than resuming into a stuck state.
Run history stays tidy. Older runs now move to an archive automatically, keeping Status and Runs fast. Nothing is deleted — restore still reaches archived runs, and Export still includes the full history. Browse it under Settings → Advanced → View archived history.
Smaller fixes:
- Sync timestamps now respect your 12-hour / 24-hour time setting, and "Last sync details" shows how long ago the last sync ran.
- New Settings → Advanced → Concurrent hashes setting to tune initial-scan speed on capable devices.
0.4.1 Jun 13
Faster first scan. Cairn can match phone photos to the copies already in Immich (by filename and capture date) and trust the server's checksum instead of re-hashing — hours down to seconds on iCloud-Optimized libraries. Optional; turn it on under Settings → Library → "Trust server checksums."
Stronger deletion safety. A run of fixes ensures cairn never proposes trashing a server photo that's still on your phone — including edited photos, Live Photos, hidden and shared assets, and capture-time rounding. Fixes the case where a photo still on your device showed up "aging out" in Pending Review.
Correctness and performance. A broad internal review hardened the deletion engine (restores, Live Photo pairs, delete-then-restore-offline), cut UI stutter, and sped up large-library syncs — including a long silent gap on the first scan of 100k+ photo libraries.
Clearer sync progress. Long syncs now show the file downloading from iCloud, live server-fetch progress instead of a frozen counter, a time estimate that accounts for iCloud throttling, and a separate scan line so the bar no longer fills and snaps back.
Reworked Settings. Clear categories with at-a-glance summaries, a quick-settings card for the knobs you revisit most, and search. Sign-in now lives under Connection.
Smaller additions:
- "Don't propagate old deletes" — clear old photos off your phone without mirroring those deletes to the server.
- "Open in Immich" opens the Immich app when installed, the browser otherwise.
- Diagnostic logging is now an opt-in toggle, off by default.
- Backup/restore now includes the hash cache, plus faster connection readings, dark-mode toggle fixes, and a more reliable launch.
0.4.0 Jun 10
Sync detail: per-asset hashing visibility. When a sync stalls on a large iCloud video, the sync detail sheet now shows the asset being downloaded — filename, total bytes, elapsed time once it crosses ~3s, plus a 0–100% download progress bar while iCloud is fetching the bytes. Replaces the previous "N of M hashed" counter that gave no signal about what was actually slow.
Reliability: photos you viewed in Photos.app no longer pile up in the deferred queue. PhotoKit bumps the modificationDate when iOS materializes an asset from iCloud for display, and cairn previously read any modDate change as needing a re-hash — a chain of view → re-hash → iCloud re-download → 60s timeout → defer with "timed out." Cairn now skips the re-hash when the asset's primary resource size hasn't changed; real edits still re-hash because the editor adds an adjustment resource that changes the size.
Reliability: transient onboarding-screen flash on launch. iOS occasionally returns "item not found" for Keychain reads that should succeed during the brief post-launch window — most commonly after a force-quit + quick relaunch. Cairn previously routed straight to onboarding on the first "not found"; the retry now covers "not found" for ~400ms before treating it as truly missing. Fresh installs and sign-outs still route correctly, just after the brief retry.
0.3.2 May 29
Incremental sync. A new Settings → Advanced toggle streams only the server-side changes since the last sync rather than refetching the full library each time. Faster on large libraries. Off by default; flip it on after signing in with your Immich email + password (Settings → Advanced → Sign in to Immich), since the streaming endpoint doesn't accept API keys. If the session is ever invalidated from elsewhere, cairn detects it on the next sync, falls back to the paginated path, and offers one-tap re-sign-in.
Time format. Settings → Appearance now has a System / 12-hour / 24-hour picker for clock times across the journal, run times, and sync timestamps. System follows iOS Settings → General → Date & Time and reacts to live toggles without a relaunch.
Smarter network errors. A failed sync now distinguishes "no connection at all" from "connected but upstream is broken" from "Immich-only unreachable" instead of always blaming the server.
Direct-LAN Immich. Cairn can now reach Immich servers exposed over plain HTTP on home-network addresses (RFC 1918 + .local) — the typical self-hosted setup.
Background syncs are more reliable. Fixes a crash class in the background-refresh handler. Background runs now share the manual-sync code path and log a "trigger=bg" field so you can tell them apart in the journal. The background-slot label switched from "Overnight" to "Background" (the slot can fire any time of day).
Manual syncs always log. Even a no-op manual sync writes to the journal so you have a record. Consecutive background-only no-op syncs collapse into a single journal row.
Deleted photos always go through quarantine. An edge-case bug let some deletions skip the 14-day window and land straight in "ready to trash." A recovery pass on every sync now catches stragglers and starts their clock fresh.
Sync detail timeline reads correctly. Earlier phases close as they complete; phases that legitimately skip (Hashing on an incremental sync with no new bytes) render as "skipped" instead of looking pending.
Tap a journal row to inspect the full run. Opens a sheet with every event from that run in one place, JSON-forward. "Last sync details" also stays available after a sync wraps.
Robust Keychain bootstrap. Transient Keychain errors at launch no longer bounce you back to onboarding when your credentials are actually saved.
Help popovers and foreground popups. Help popovers got a full perimeter outline including the arrow tip plus more inner padding — they read as defined cards rather than blending into the screen. Two transient error alerts that briefly flashed on app activation are fixed.
Accessibility. Dynamic Type works across the UI (text scales with the system text-size setting). VoiceOver announces sync phases and reads custom toggles, progress bars, and pickers properly. Destructive-action buttons meet Apple's 44pt tap-target minimum. Light-mode muted text now meets WCAG AA contrast.
0.3.1 May 28
Find missed deletions: a new Settings → Recovery scan that catches photos you deleted from your phone before cairn had a chance to record them. Configurable date range; "Stricter filter" requires positive evidence that cairn previously saw the photo on this device — eliminates false positives from server-side albums.
Shortcuts integration: trigger cairn syncs from the Shortcuts app. Build personal automations like "after Camera closes, sync with cairn" to keep things in sync without opening the app.
Background sync reliability: cairn was silently failing to schedule background slots due to an internal bug. Fixed — should now run consistently in the background between manual opens.
Sync journal: every sync row now shows what triggered it (trigger=manual, trigger=background, trigger=shortcut, etc.) so you can verify background fires actually happened.
Smaller fixes: cleaner Recovery sheet header, export picker no longer drifts on iOS 26, more robust handling of Keychain hiccups on app launch.
0.3.0 May 21
Offline
- Sync fails fast when Immich is unreachable rather than spinning indefinitely.
- The "Couldn't reach Immich" alert pops once per disconnected session, not on every retry.
- Photos deleted on iPhone while offline are recorded locally and surfaced after the sync attempt; they propagate on the next successful sync.
- Trash decisions made offline (from Pending Review or after a dry run) persist in a retry queue and execute automatically when the connection returns. Retry limit is tunable in Settings.
Onboarding
- The Continue button is no longer greyed at any step. With empty fields, tapping it focuses the first missing input; with fields filled but unverified, it runs Verify on your behalf and advances on success.
- The server URL field auto-focuses on arrival and accepts bare hostnames (immich.example.com works — no https:// required).
Other
- New About cairn screen in Settings shows the marketing version, build number, and license.
- Failed runs in the Runs tab show the actual reason instead of a generic "stopped by safety rail" label.
- Brand header now reads the live system status bar height instead of using hardcoded per-device padding.
0.2.0 May 14
Fixes trash requests that could get stuck permanently.
If a trash run was interrupted partway through — server unreachable, connection dropped mid-sync, VPN down — the queued retry could wedge and report "A tag with that name already exists" on every attempt after that, and no amount of retrying could clear it.
Every trash run writes a breadcrumb tag to Immich so the run can be reconstructed later, and cairn was creating those tags with an endpoint that rejects duplicates rather than the one that reuses an existing tag. Because a retry reuses its original run's tag, every attempt after the first was refused.
Retries now reuse the run's existing tag instead of failing on it, and any request already stuck this way clears itself on the next sync after you update. Run breadcrumbs also nest properly under cairn / v1 / run in Immich's tag list now, instead of appearing as one flat entry.
more Version 0.4.4 Aug 20
Data Not Collected The developer does not collect any data from this app.