---
title: PhotoStructure v2026.9
url: https://photostructure.com/about/v2026.9/
description: PhotoStructure v2026.9 release notes
date: 2026-09-10
keywords: release-notes, release-notes-2026, release-notes-2026-q3, previews, sync, health-checks
---

## What’s in this release

**If you run PhotoStructure for Node, you need Node.js 24 before you upgrade** — see Breaking changes below. Docker and the desktop apps need nothing from you.

Sync is a lot faster with larger libraries: one index was scanning its whole table on every file saved, which held imports to roughly one photo per second no matter how good your hardware was. Capture times are now shown in your timezone rather than the server’s, Related Assets rows scrub and page like the main filmstrip, and stored GPS locations open in your map app. You can switch off a health check that’s wrong for your machine, the Linux desktop app no longer crashes on startup, and crash reports no longer carry your home directory or your computer’s name. And if your photos live in Apple Photos, the albums, named people, keywords, titles, and corrections you made there now come along.

## v2026.9.1-beta — September 10, 2026

### ✨ Highlights

- **Photos in an Apple Photos library bring their albums, people, and corrections with them.** Photos keeps much of what you’ve done to a picture in its own database rather than in the picture itself: which albums it’s in, who you’ve named in it, keywords, titles and descriptions, and any date or place you corrected in the app. PhotoStructure used to see only what was inside the file. When it finds a photo inside a Photos library on your Mac — or in an iPhone or iPad backup — it now reads that database as well (it never writes to it), so all of that shows up in PhotoStructure. What the file itself says still wins; Photos only fills in what’s missing. Apple’s street-address place names build the Where hierarchy for these photos, down to a new point-of-interest level for the park, business, or harbor you were at. Copies PhotoStructure organizes into its own library keep all of it, a later correction in Photos reaches the copy too, and the copy is named after the file’s original name (`IMG_6377.jpeg`) rather than the `B4863652-EBA8-…` name Photos uses on disk. Reading Photos’ database needs a PLUS license and, on a Mac, Full Disk Access for PhotoStructure.
- **Sync is faster.** The similarity-search index stored its rows under ids of its own choosing, so the cleanup PhotoStructure runs every time it saves a file had to scan the entire index to find the right one — 426ms per save at 416,000 files, which held sync to roughly one file per second regardless of how fast the machine was. Three other things went with it: importing a copy of a photo you already have no longer redoes the whole finalize pass, index compaction is skipped when nothing was deleted (about 2.5 seconds a run, every two minutes through most of a sync and 285 times in the busiest hour observed), and the fixed one-to-two core reserve is gone, so an 8-core machine now gets 6 import slots instead of 4. Thanks to B!z0 and Leaky for reporting these issues.
- **Capture times are shown in your timezone, not the server’s.** A photo taken at 5:26 PM in New Zealand read as 5:26 AM on a UTC server — while still labelled “UTC+12”. PhotoStructure now sends your browser the wall clock and the capture’s own UTC offset and lets the browser do the formatting, so times are right wherever you’re viewing from and in whatever locale your browser uses. Thanks to Tim for reporting this issue.
- **Pick a 12- or 24-hour clock.** No browser will tell a website whether your computer is set to 24-hour time — it only reports what’s conventional for your language — so the nav menu has a **Clock** item that cycles **Auto**, **12-hour**, and **24-hour**. **Auto** follows your locale, which is what you had before. Times already on screen keep their old format, so the toggle raises a toast with a **Refresh** button.
- **Related Assets rows browse like the main filmstrip.** Each row scrubs on hover, wheel, and drag; a click or a settled scroll opens that photo; and every row pages its own edges and keeps its place as you move around. Rows built from several tags now mean *all* of those tags, resolved by one exact query rather than whatever happened to overlap first, and a pill names the tags in play. On a phone the photo stays put below the header while the rows slide over it, so scrubbing a lower row no longer pushes the picture you’re previewing off screen.
- **Resting on a filmstrip thumbnail sharpens the preview.** Hovering painted the smallest preview full-screen and never improved it, however long you left the cursor there — while on a phone, letting the strip settle loaded the sharp one. A cursor at rest for a third of a second now upgrades the preview in place: no navigation, no history entry, and sweeping across the strip still costs nothing. Thanks to ukanuk for reporting this issue.
- **Open a photo’s location in your map app.** When a photo carries GPS coordinates, the info panel now offers them to Apple Maps, Google Maps, and OpenStreetMap, plus whatever map app your device has registered. Thanks to Baku, tkohhh and ukanuk for reporting this issue. See the [forum thread](https://forum.photostructure.com/t/1044) and [discord](https://discord.com/channels/818905168107012097/1126996290757206016/threads/1541952782867636224)
- **Crash reports no longer carry your home directory or your computer’s name.** If you’ve opted into error reporting, stack traces, command lines, and console breadcrumbs all included absolute paths — `/Users/yourname/…`, `C:\Users\yourname\…` — and the reports were labelled with your machine’s hostname, which on a home computer is routinely a person’s first name. Paths are now replaced with symbolic roots (`$HOME/…`, `$LIBRARY/…`) before anything leaves the process, the hostname is hashed so installations can still be told apart, and the stack-variable capture that could have picked up a license key or an email address is switched off.
- **Google Pixel RAW captures no longer import twice.** Every Pixel 10 RAW shot showed up as two photos — the camera JPEG and its DNG — because the pair’s capture times are a second apart, their ISO values differ, and their filenames defeat the usual matching. PhotoStructure now compares exposure settings in physical stops and uses brightness rather than color evidence, so a RAW rendered through broken color data still matches its JPEG, without pulling in the burst frame that happened to be nearby. Thanks to Tim for reporting this issue.
- **Turn a health check off from the health page.** When a check is simply wrong for your machine — a bogus temperature sensor pausing sync is the usual one — your only way out was editing `settings.toml` and restarting. Every row on the health page now has an Enable or Disable control. Disabling a check that was stopping sync releases sync right away, with no restart. Critical checks ask for confirmation first, and the controls are hidden when the setting is pinned by your environment.
- **The Activity page shows why imports are throttled, not just how many are running.** “24 tasks running, 16 queued” can’t tell you whether PhotoStructure is holding slots back because your machine is busy or simply hasn’t filled the queue — two situations that look identical and call for opposite responses. There’s now one box per processor core, coloured by what that core is doing: running an import, free, held back for CPU load, or deliberately reserved so the machine stays usable while you’re on it.
- **Phones are named, not numbered.** Xiaomi, Huawei, Oppo, Vivo, Realme, Sony, Asus, HTC and ZTE write an internal part number where you’d expect the phone’s name, so photos filed themselves under `M2102K1G` or `VOG-L29` instead of the phone you actually own. Those now resolve to real names, and Samsung’s list — which stopped at the S23 — is current again. Three long-standing mistakes went with it: an `SM-A30` code claimed the wrong Galaxy, a `VS98` code claimed four LG models it wasn’t, and one Sony camera rendered as “DSC- II”.
- **A long video transcode no longer fills your disk.** A transcode that ran past the per-task time limit expired every task queued behind it. Enough of those tripped the threshold that restarts sync; the restart orphaned `ffmpeg`, which kept writing its full-size temporary file, and the next sync started the same video over again. One reporter’s library accumulated 415 GB of abandoned transcode output from a single video in six days. The time limit now starts when the task starts rather than when it’s queued, and leftover temporary files are swept a minute into each sync instead of on a half-hour timer a restarting process never reached. Thanks to JOINSO for reporting this issue.
- **Sync can be manual-only.** Set the sync schedule to `disabled` and PhotoStructure stops running its nightly scan. The first sync of a new library, an interrupted run, and anything you ask for by hand all still work. Thanks to OnlinePerson for reporting this issue.

### ⚠️ Breaking changes

- **PhotoStructure for Node now requires Node.js 24 or newer.** Node 22 is no longer supported. **This only affects the Node edition**, where you install Node yourself — the Docker image and the macOS, Windows, and Linux desktop apps bundle their own copy and need nothing from you. If you’re on the Node edition, upgrade Node before upgrading PhotoStructure; on an older Node the Tools section of the health page will tell you so.
- **`exposureThresholdPct` (and its old alias `minExposureSettingsCoeffPct`) has been removed.** One percentage could not mean the same thing for aperture, shutter speed, ISO, and focal length. Exposure tolerance now follows `aggregationMode`: `strict` allows 1/16 stop, the default allows 1/3 stop, and `lenient` allows one full stop. Either old name still starts PhotoStructure — it warns and points at `aggregationMode`.

### 📦 Other improvements

- **The info panel says when it’s still loading, and how recently each file changed.** It used to open blank and stay blank while the details were fetched — which you’d notice holding down an arrow key, since each repeat moves to the next photo before its details arrive. There’s a spinner now, a failure offers a retry rather than spinning forever, and each file row carries a coarse “Updated 5m ago” line that keeps itself current. The redundant rounded megapixel count is gone; the exact dimensions stay.
- **“Re-sync this asset” works while sync is paused.** Pausing sync from System Activity also stopped the things you’d explicitly asked for. Only the manual pause is waivable — a low battery, a suspended machine, CPU pressure, a failing health check, and shutdown all still stop everything.
- **System Activity’s progress panel is quicker.** Its recent-activity query had no index to work with and scanned the file table on every poll: 193ms in the field, and 35.9ms → 0.37ms on a 100,000-asset reproduction. Thanks to Michael Kahn for reporting this issue.
- **Sync report links keep working after the report is gone.** System Activity keeps its per-directory counters longer than it keeps the reports behind them, so those links eventually opened a 404 page. PhotoStructure now checks first and says “No records could be found” in the app instead. Thanks to Leaky and Zandr for reporting this issue.
- **The health page checks jpegtran itself.** That row sat at “Checking jpegtran…” whenever the sync process wasn’t pushing updates — which is exactly when you’re most likely reading the page. Its version check was also dead, so an outdated libjpeg-turbo never showed as unsupported.
- **First-run setup resumes where you left off.** Welcome and plan choices are now recorded in the installation’s config, so a half-finished setup picks up from the decisions you already made rather than starting over, and a page left open from a library you’ve since switched away from is rejected instead of acting on the wrong library.
- **The desktop apps confirm a saved report.** Downloading a scanned-folder report from the desktop app saved it silently; there’s a toast now. Thanks to Leaky for reporting this issue.

### 🐛 Bug fixes

- **The Linux desktop app no longer crashes on startup.** The v2026.8 apt build crash-looped on Ubuntu 26.04: both background processes segfaulted about five seconds in, forever, on the first image it tried to decode. Electron’s Linux binary and PhotoStructure’s image library each loaded their own copy of a shared system library, and an object made by one was freed by the other. PhotoStructure now builds and ships a patched pair.
- **One unreadable file could wedge a sync that then never finished.** A file blocked as problematic was skipped without ever reporting a result, so the producer waiting on it kept counting it as in flight — and the wait for the directory to finish, which has no timeout, never returned. Skipped files are now reported as skipped, and they show up in the sync report’s error download with the reason, so the count and the download agree.
- **A paused sync says what’s actually wrong.** The System Activity page showed the raw text of these messages — asterisks, code fences and all — ran multi-line explanations together into one paragraph, repeated a failure that already had its own row above, and labelled the row with the subsystem rather than the problem. A stopped sync now reads “Not enough free disk space” instead of “Disk space”, once, formatted.
- **Windows: JPEGs in folders with non-English names import again.** The bundled JPEG tool couldn’t read paths it was handed on the command line when they contained non-ASCII characters, so those photos failed. PhotoStructure now opens the files itself and hands the tool an already-open file.
- **A stalled library no longer writes a huge sync report.** A paused sync wrote the same “Tasks are paused” line every few seconds for as long as it stayed paused, which is how one unchanged condition produced a multi-megabyte report. Repeats now collapse into “Last message repeated N times”.
- **Remaining-work counts and the time estimate no longer understate the backlog.** Files buffered and running were counted one way on the Activity page and another way in the estimate. Import estimates are also hidden while scanning, while paused, and during post-import work, where they meant nothing.
- **A blocked sync can’t hide.** A condition serious enough to stop sync could be suppressed from the health page when a different, unrelated fault outranked it — a full disk under a database error appeared nowhere at all. Every blocker is now accounted for, and a diagnostic that contains a file path no longer mangles the path it’s complaining about.
- **macOS: running from outside Applications no longer breaks updates silently.** PhotoStructure can only replace its own app bundle when it’s installed in Applications — run straight from the disk image, updates could never apply. It now says so at startup and skips the update check instead.
- **Hand-edited `settings.toml` survives a save from the web UI.** Every writer rebuilt the whole file from what that process had in memory, so a value you added by hand — or one another page had just changed — was silently replaced. Saves now read the file first, and the settings form is rejected outright if the file changed underneath it.
- **One stray warning no longer takes down the whole app.** Any deprecation warning printed by any dependency during worker startup was read as a failed launch, which force-killed the worker and cascaded into the web and main processes dying.
- **Monthly and less-frequent sync schedules run once, not continuously.** Node turns any timer longer than about 24.8 days into an immediate one, so a sparse schedule re-ran maintenance over and over, and a long `syncMaxUptime` could restart sync in a loop. Thanks to OnlinePerson for reporting this issue.
- **Settings showed the wrong answer for update checks and error reporting.** Opening the settings page replaced your saved choice with “unset” and re-checked “Yes” — only on the page; the values in `settings.toml` were correct all along. The page also now names `optOut` or `noNetwork` when one of those is what’s holding these off. Thanks to OnlinePerson for reporting this issue.
- **Files on an unreachable drive no longer spin the sync queue.** When a remembered path resolved but the drive wasn’t currently mounted, the file was re-selected on every pass and never marked done — about 6,700 files whose volume identities were orphaned by an OS reinstall, cycling forever. The row and its pending work are kept, so a later scan retries when the drive returns.
- **Sync counts on the System Activity page add up again.** Two things threw them off. Work done by everything else in the pipeline was attributed to the scan whenever a scan root contained the library — one library recorded 2,618,353 processed images against 146 discovered files. Separately, a run that resumed an interrupted one added that run’s totals to its own, and a resume re-walks the tree, so another library reported 11,075 files against 5,513 actually on disk and 1,535 phantom files “in progress”. Every counter is now per-run, and the page says so.
- **Database backups respect their retention setting.** How many backups survived depended on timing rather than on `dbBackupRetentionCount`, because the count was only enforced as a side effect of an age-based cleanup that skips anything under an hour old. Thanks to OnlinePerson for reporting this issue.
- **Docker and TrueNAS: a container started as a numeric uid with no `/etc/passwd` entry now starts.**
- **Windows: no more console windows flashing** while PhotoStructure searches its own logs or watches your folders for changes.
- **Windows: the library write check no longer collides with itself.** Thanks to Leaky for reporting this issue.
- **The current photo stays in the filmstrip after you close Related Assets.** A thumbnail request still in flight from before you navigated could restore the old window, leaving the photo you’re looking at absent from the strip entirely. Thanks to Tim White for reporting this issue.
- **Opening Related Assets no longer scrolls the photo out of view.** Centering a thumbnail scrolled every container it sat in, not just the strip.
- **The breadcrumb’s photo count is visible on phones again.** The “View all assets” link at the end of the breadcrumb was tagged to hide below 540 pixels wide, so on a phone the count simply wasn’t there. Thanks to tkohhh for reporting this issue.
- **A photo whose capture time had to be guessed no longer has its sidecar rewritten every sync.** When PhotoStructure infers when a photo was taken, it records that date in the photo’s companion sidecar file. It then compared the date it had written, as text, against the date it read back, as a date — never equal — so every sync rewrote the sidecar, which moved the file’s modification time, which sent the photo back through the copy-and-repair pass. A sidecar that already says the right thing is now left alone.

* * *

### Detailed changelog

#### ✨ New features

- **`assetFileUriSchemes` chooses which URI schemes may identify a file.** PhotoStructure records where each file lives as `pslib:`, `psfile:`, `psnet:`, or `file:`, tried in that order — most durable identity first. Operators whose volume identities are unstable — a NAS whose hostname changes, say — had no way to stop those schemes re-importing their files as duplicates. This setting is an allowlist over all four, defaulting to all of them, so you can drop just the one that’s hurting you. Dropping any of the first three quietly weakens what PhotoStructure knows about a file — it falls back to the next scheme. Dropping `file` is different: a file no remaining scheme can identify isn’t imported at all, and is warned about and reported as `excluded`. Changing this rebuilds the library so URIs and folder tags migrate together. **Read [the file URIs guide](/guide/file-uris/) before changing it** — a `file:` URI records no volume, so an unplugged drive becomes indistinguishable from deleted files.
- **Health checks can be skipped from the UI.** `skipHealthCheckIds` was already the setting; the health page now edits it. Each toggle replays its single change onto the persisted list rather than rewriting the file from memory, and relays the new list to the running sync over a new RPC, so a check that was holding sync in `stop-sync` releases it without a restart. A check you turned off reports a distinct user-disabled level, which is how the page knows to offer Enable only where you were the one who disabled it.
- **`syncCron` accepts `disabled`.** Regularly scheduled syncs stop; initial, unfinished, and manually requested runs continue. The mode is reported on the System Activity page and by `photostructure info`.
- **GPS coordinates are stored as latitude and longitude.** `AssetFile.geohash` is replaced by two nullable `REAL` columns normalized to six decimal places, so PhotoStructure keeps the precision your camera recorded instead of a 30-bit cell. Existing libraries are upgraded in place by decoding their geohashes into cell centers; those rows are flagged, the info panel marks their location approximate, and a normal re-sync clears the flag once the file’s real coordinates are re-read. Measured on a 212 MiB library with 304,540 file rows and 146,112 geohashes, the migration took 895–1,396 ms.
- **Slow-query warnings say why the query was slow.** A 193ms query reported from the field couldn’t be diagnosed from the log alone, so the warning now carries SQLite’s per-statement counters. Note that a non-zero `fullscanStep` does not mean “missing an index” — a covering-index plan reports one too. Thanks to Michael Kahn for reporting this issue.
- **Related Assets rows carry a server-owned context.** Every row has a canonical identity (`tags:<sorted ids>`), so navigating from a merged row keeps you in that row, and arrow keys and filmstrip paging share one resolver. Rows built from several similar single-tag windows are grouped deterministically, at most four tags to a row, and rendered from one intersection query. A parent-tag scope no longer returns the same photo once per descendant tag.
- **The original file is offered at the preview plan’s render width, and withheld until you zoom.** Every asset now advertises a resolvable original at that width — including HEIF and RAW, which the old gate excluded because it tested whether the browser could decode the source rather than whether `/actual` could convert it — without displacing a generated preview at the same width. The zoom gate matters: the viewer strips that candidate at minimum scale, so large displays don’t pay an on-demand source render on every navigation. A deliberate zoom re-offers it.
- **Apple Photos metadata, in detail.** `applePhotosMetadata` (on by default) reads `Photos.sqlite` for any file inside a `*.photoslibrary` bundle or an iPhone/iPad backup’s `PhotoData` tree. The database is opened read-only, and its schema is discovered from `sqlite_master` at read time rather than pinned to a Photos version, because Apple renumbers its join tables with every revision. Named people arrive as `PersonInImage`, keywords as `Keywords`, and titles and descriptions under their standard names; albums come in as `applePhotosAlbums`, now part of `tagAlbumTitle`. Photos’ own capture time joins `capturedAtTagsSecondary` as `applePhotosDateCreated`, so the file’s primary date still wins. Apple’s reverse-geocoded address leads every level of `tagGeoSynonyms` except the state, where ExifTool’s “California” beats Apple’s “CA”, and `tagGeoTemplate` gains a `POI` level that is empty — and skipped — for everything else. A new `tagGeoCollapseRepeatedLevels` (off by default) drops a level that repeats the one directly above it; it stays off because New York’s state, county, and city are three different places. An organized copy sits outside the bundle, so its Apple-only fields travel in its XMP sidecar under standard tag names, and each write records what the copy owned and what was written — which is what lets a correction in Photos reach an existing copy, a removal clear it, and a manual edit survive. Copies take their stem from `PreservedFileName` with the file’s actual extension, since Photos transcodes on import, and the date-from-filename fallback reads that name too. Reading the database is gated on a PLUS license; on macOS it also needs Full Disk Access.

#### 🐛 Bug fixes

- **The settings page shows the effective value of an overridden setting.** When a setting is pinned by an environment variable or the command line, the page now shows what PhotoStructure actually resolves — including `originalsDir`, which had been displaying the raw override rather than the path files are really written to — and keeps the control read-only rather than letting an unrelated save overwrite the stored value underneath it. A re-render after a failed save keeps what you just typed instead of resetting to the stored values. Thanks to ukanuk for reporting this issue.
- **Capture times are formatted in the capture’s own zone.** `toLocale*()` renders an instant in the server’s `TZ`. PhotoStructure now formats the Luxon `DateTime` in its own zone, and when an asset has no recorded capture zone it omits the offset entirely rather than reporting the server’s as the file’s. Thanks to Tim for reporting this issue.
- **Settings are never reported as saved when they weren’t.** Resolving the config directory could come back empty — a broken Docker mount is the usual cause — and the write then returned quietly, so the caller reported success and nothing was persisted. Saves without durable storage are now rejected outright, and one memoized resolution is shared across every caller so a failed snapshot cannot later overwrite a settings file it never read.
- **The library database refuses to open where it cannot be safe.** A connection that can’t enter WAL mode is now closed and the error propagated, rather than running on in a journal mode the rest of the code doesn’t expect. Write transactions that read before they write also reserve write ownership up front, so a competing commit can’t invalidate what they just read; nested savepoints behave as before.
- **Available memory is read from the OS.** Free-memory throttling used the kernel’s free-page count, which on Linux excludes reclaimable cache and so understated what was actually available.
- **A malformed XMP history record says so once.** A sidecar whose inferred-history record carried a bare date where JSON was expected was skipped in silence, so a file that lost its inferred capture time left no trace at any log level. One warning per read now names the source file and which fields were ignored — never the payload itself. Valid neighbouring records still infer, and records with no `Parameters` at all stay quiet, because there was never anything to infer there.
- **Two web processes sharing a log directory no longer truncate each other’s access log.** Both opened `webrequest-001.jsonl`; measured with two processes writing 200 entries each, one file survived holding 200 and the other 200 were gone. Access logs are now named per process.

#### 🏗️ Under the hood

- **The similarity-search index keys its rows by file id.** `sqlite-vec` pushes constraints down to rowid and vector KNN only, so the `DELETE` that runs on every file save scanned the whole virtual table — 426ms per statement at 416,000 rows. Rows now live at `+assetFileId` (rendered) and `-assetFileId` (canonical rotation), which puts those deletes on a point-lookup plan. An index built before this change can’t lose a row to an unrelated file’s save: the delete requires the row to carry that file’s id. An index written by an earlier release still holds rows at ids this build now hands to other files, so a one-time structural migration drops and recreates that derived table on upgrade, and the usual coverage check refills it — closing the window where a save could otherwise hit the old table and roll back. Thanks to B!z0 for reporting this issue.
- **Importing a byte-identical copy skips the full finalize pass.** When an orphan file is adopted into an asset that has already settled and its bytes match an existing sibling, nothing about the asset changes except the tags implied by the new file’s path — but finalize re-did preview reconciliation and re-read every sibling’s tags to conclude that. In one user’s 53.2-hour import, 94% of adoptions were SHA matches and finalize consumed 106.4 CPU-hours, half the pipeline. Those adoptions now run an O(1) `fs|` tag reconciliation instead.
- **The fixed CPU reserve is gone.** `AdaptiveConcurrency` already regulates admissions against measured system-wide CPU, so subtracting one to two cores on top only held the ceiling below the configured `cpuBusyPercent` budget with no way to recover — an 8-core default host goes from 4 import slots to 6, and `cpuBusyPercent=0` now genuinely means all cores. Thanks to Leaky for reporting this issue.
- **Vector-index compaction is skipped when nothing was deleted.** Active maintenance fires on whichever comes first — two minutes elapsed, or 250 completed tasks — so it runs about every two minutes through most of a sync, and the task trigger takes over when work is heaviest. Across one user’s reports that was 1,949 runs at a median of 2.5s, peaking at 285 in a single hour. Imports are almost entirely inserts, and `optimize` only compacts after deletes, so nearly all of that was wasted. Deletes are now counted per connection and table and a clean table is skipped; forced admin maintenance still always optimizes. Thanks to Leaky for reporting this issue.
- **Roboto ships as one variable font.** A single 100–900 weight axis renders intermediate weights accurately and packages smaller than the separate faces it replaces.

## 🤔 Questions? 🌹 Feature requests? 🐞 Found a bug?

Visit the [**official PhotoStructure forum**](https://forum.photostructure.com) or [**Discord**](/go/discord)!
