A Digital Light Table for the Linux desktop: Immich + open source RAW editors = BrightTable.
Copyright (C) 2026 Rob Brown
above:Auto-stacking images: stacking is performed locally, but synced with Immich via the Immich API. One of the many features of BrightTable.
- 🔗 Sits on top of #Immich as the DAM backend.
- 🖼️ Round-trip RAW editing with #ART, #RawTherapee, or #darktable. Open, edit, close, done!
- 📋 Copy/paste image processing & metadata across photos
- ⚡ Headless batch reprocessing: apply RAW edits to whole selections without opening a GUI
- 🔍 Loupe: hover-magnify thumbnails for that light-table-and-loupe feel
- 🗂️ Smart Stacking: auto-groups RAW+JPEG pairs, edit versions, or burst shots time (sub second to 1 minute).
- ⭐ Fast culling: star ratings, favorites, and rejects
- 📤 Export to folder or share straight to Flickr (other sites coming soon)
- 🖨️ Built-in printing with paper size and preview.
- ⌨️ Customizable keyboard shortcuts
I designed this for myself because I dislike the user experience and functionality that exists on GNU/Linux for managing and editing photos. For me, this fills the gap between DAM and raw editing - an experience that I have been pining for since the great Apple Aperture bit the dust a decade ago.
There are some great tools out there already like RawTherapee/ART, Digikam, RapidRAW, Digikam, Shotwell. But to me, none of them had exactly the user experience and workflow that I really wanted. So after a couple of years of threatening myself to build my own tool: I did.
What I was yearning for was a tool to tie the raw editing power of ART/RawTherapee with the server based DAM of Immich. Then wrap it in a first class user experience like Shotwell has.
Let's call this need a 'digital light table'.
- The library: Uses Immich and the Immich API as a self hosted asset manager backend. It will not work without Immich!
- Processing: Uses open source RAW editors for photo processing e.g. ART, RawTherapee.
- The light table (this app): A front end app that uses the above, and creates a (I think) great user experience for editing your photos.
An LLM coded project. Human generated requirements and testing. While this tool uses LLMs to generate code, the concept, requirements, and testing is performed by the author. This is Human Concept --> Requirements --> LLM Coding --> Human Testing.
If you disagree, take your neo-luddite principles elsewhere.
If you decide to use this, please read the warnings below.
-
This tool was created by me, for me. I am giving it to the community to allow others who are interested to use it, and accept the risks involved.
-
Use at your own risk! You are responsible for using this tool. Read the code and understand what it does before using it. Test it on a sandbox first.
-
If you use Immich, and use the excellent opensource RAW editing tools: ART, RawTherapee, Darktable, then you will be able to use this tool. If you don't, then sorry this is not for you.
-
As they say, backup backup backup!
-
I may or may not address bugs reported by the community. I have a full time job and this is very much a side project in my spare time. I don't have the time to deep dive bugs encountered because of differences in your setup. Sorry.
-
Features are the enemy of quality! In the interest of keeping it simple I am unlikely to respond to new feature requests. You are welcome to submit a pull request with a bug fix, or a feature request that you are willing to implement yourself. However I want to keep this application primarily a simple, quality-focused tool with a user experience that is easy to use and understand. I therefore may reject pull requests for new features if I don't feel they fit the project's goals.
-
This all said, you are welcome to fork the code! Add your own features to it, repackage it, do something completely different. Consider this a concept and take it in the direction you want it to go.
- This is NOT a standalone app — it requires the following to be configured. It may take you 5 mins to 30 mins to set up depending on your experience and what you have already configured. Once it is set up there is no repeat configuration.
- You must have an Immich Server instance running. BrightTable will access it via its API.
- For roundtrip photo editing (with ART, RawTherapee or Darktable) you must have those applications installed (obviously).
- For roundtrip photo editing you must have an Immich External Library folder set up in your Immich configuration, and that same folder must be mounted on your local system (via SMB, NFS). It will NOT work if you just use Immich's default library location! More advanced photography users may already have an external library set up on their NAS / homelabsystem as a folder of images that they organize. Giving Immich and your local desktop access to this folder will allow you to use it with BrightTable.
- Linux desktop only. Designed and tested on GNU/Linux — while it could technically be extended to Mac or Windows, I don't have either platform to test.
- Flatpak: just needs Flatpak itself installed — most modern distros ship it by default; otherwise
sudo apt install flatpak(Debian/Ubuntu) or your distro's equivalent. - Since Flatpak is a sandboxed container, you may need to configure your system to allow BrightTable to access all of the necessary system resources (e.g. network, file system). Flatseal is a recommended app to allow you to configure these permissions. Instructions are provided below.
- To access your library away from home, you must set up Tailscale and have end points configured for your Immich server container, plus the network share for the external library folder.
Install the Flatpak below, then read the User Guide to connect BrightTable to Immich and start using it.
1. Download
Go to the production release and download the file ending in .flatpak (e.g. BrightTable_<version>_x86_64.flatpak).
2. Install
- Open Terminal / Console / Konsole (choose your command line of choice):
(Drop
flatpak install --user ~/Downloads/BrightTable_*.flatpak
--userto install system-wide instead — needs root via polkit.)
3. Grant folder and network access (required)
Install Flatseal from your software center (if not already installed). Then:
- Grant network access BrightTable can't reach your Immich server without this — it isn't granted by default.
- Select BrightTable, and turn on the Network toggle.
- Command line:
flatpak override --user --share=network io.github.brownphotographic.BrightTable
- Grant access to the photo library folders If your photo library lives outside your home folder (e.g. an NFS mount), you'll need to grant that path too. There are two folders you will want to expose
- Immich managed library. In the settings
- Optional (and recommended to set up) - for externally managed folder (e.g. a network share that you can expose to immich https://docs.immich.app/features/libraries)
To do this, in Flatseal scroll down to Filesystem. In the Other files, enter the paths. e.g.
- /mnt/nfs/path/to/your/immich-managed-library-share
- /mnt/nfs/path/to/your/own-externally-managed-library-folder
More more details see What this actually does under the hood under Developers below.
-
Grant access to /tmp under Filesystem if not already granted
-
If you print, grant access to Printing System under Socket
4. Launch
Look for "BrightTable" in your app menu, or run:
flatpak run io.github.brownphotographic.BrightTableNot showing up in your app menu? Flatpak installs export a
.desktopfile automatically — no extra step needed — but your desktop environment only picks up new ones from a fresh session. Log out and back in (or reboot), then check again.
Not on the AUR yet — this repo ships a PKGBUILD you build locally instead. It compiles the app from
source on your machine (same recipe as the Flatpak build, just installed natively via pacman rather than
into a sandbox) and installs it like any other Arch package: tracked by pacman, cleanly removable, no
container.
git clone https://github.com/brownphotographic/BrightTable.git
cd BrightTable/packaging/arch
makepkg -simakepkg -si builds and installs in one step, pulling in depends/makedepends for you (confirm each
with pacman/sudo prompts as usual). Once installed, launch it the same way as any other app:
brighttableSince this is a native build rather than a Flatpak, there's no sandbox — no Flatseal step, no
--filesystem= grants needed. It reads whatever files your user account can already access.
This PKGBUILD tracks tagged releases (
pkgveris bumped automatically alongside the app version — seeapp/scripts/bump-version.mjs). It is not yet published to the AUR, sopacman/yaywon't find it by package name; you have to clone this repo and build it yourself as shown above.
Once launched, go to the Preferences menu (Edit >> Preferences) to configure the app. Visit the user guide for more information on configuring and using the app. User Guide
- Rust via rustup — Debian/Ubuntu's packaged
rustcis too old for Tauri 2.x, so use rustup even on Linux. - Node.js 20+ (Debian/Ubuntu's apt package is often too old — use nvm or your distro's Node setup instead).
- Tauri CLI:
cargo install tauri-cli --version "^2". - Linux system packages (Debian/Ubuntu names — adjust for your distro):
sudo apt update sudo apt install -y libwebkit2gtk-4.1-dev libglib2.0-dev build-essential curl wget file \ libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev pkg-config
cd app && cargo tauri devCompiles from source inside flatpak-builder's own sandbox, against the GNOME runtime/SDK's own
toolchain and glibc — the result's compatibility floor is whatever GNOME runtime version it's built
against, the same for every user regardless of which machine ran the build.
One-time host setup:
sudo apt install -y flatpak flatpak-builder # or your distro's equivalent
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y flathub org.gnome.Platform//50 org.gnome.Sdk//50 \
org.freedesktop.Sdk.Extension.rust-stable//25.08 org.freedesktop.Sdk.Extension.node22//25.08The two SDK extensions need that explicit //25.08 branch — org.gnome.Sdk//50 doesn't declare an
automatic version remap for them (confirmed via flatpak info --show-metadata org.gnome.Sdk//50), and
Flathub only publishes them under freedesktop-sdk's own year.month branches, never under GNOME's "50". 50
happens to be built against freedesktop-sdk 25.08 (visible in its own org.freedesktop.Platform.GL/
.Timezones extension points), so that's the correct, ABI-matching branch to pin — not a workaround. The
manifest's add-build-extensions block pins the same branch for the actual build; if a future GNOME runtime
bump changes this, flatpak info --show-metadata org.gnome.Sdk//<version> is how to find the right one
again.
Then, from app/:
npm run build:flatpak(alias:npm run build:full) runs the full release pipeline: prompts for the app version and the Immich server version this build was tested against (or readsAPP_VERSION/TESTED_IMMICH_VERSIONfrom the env non-interactively), bumps versions, regeneratesTHIRD-PARTY-LICENSES.md, then builds. Version convention:First.Second.Third— First = sweeping changes, Second = new features (keeps incrementing, e.g.1.101.0is fine), Third = bug fixes.npm run build:flatpak:onlyskips straight to theflatpak-builder/bundle step, reusing whatever version is already inCargo.toml— no prompts, nothing else touched. Use this while iterating/debugging the build itself so you're not re-answering prompts or regenerating the license file every run.- Set
TESTED_IMMICH_VERSION(e.g.TESTED_IMMICH_VERSION=3.0.1 npm run build:flatpak) so the version bump also updates the About dialog's compatibility line. Still update COMPATIBILITY.md by hand afterwards — that part isn't automated. - Output lands at
app/src-tauri/target/release/bundle/flatpak/BrightTable_<version>_x86_64.flatpak.
The Flatpak build above needs none of this - it compiles straight from your working tree, tag or no
tag. packaging/arch/PKGBUILD is different: its source= downloads a tarball from a published GitHub
tag, never your local checkout. A version only becomes makepkg -si-installable once a tag exists
that points at a commit which actually contains the bump.
The gotcha: git tag snapshots whatever the last commit was, not your working directory or whatever
npm run build:flatpak just wrote to disk. Tag before committing and the tag silently points at the
previous release's commit instead - e.g. a "1.2.1" tag whose downloaded source is still 1.2.0. So the
order has to be commit-then-tag, never the reverse:
# 1. Bump (npm run build:flatpak) and build/test the Flatpak - no tag needed yet.
# 2. Commit the bump once you're happy with it.
git add -u && git commit -m "X.Y.Z"
git push origin main
# 3. Only now tag the commit that actually has the bump.
git tag X.Y.Z # match Cargo.toml exactly - see PKGBUILD's own tag-naming note above
git push origin X.Y.Z
# 4. The real checksum only resolves once that tag is live on GitHub.
cd packaging/arch && updpkgsums
git commit -am "X.Y.Z: real sha256sum" && git push origin mainAlso update COMPATIBILITY.md by hand - that part isn't automated.
-
Install the bundle:
flatpak install --user src-tauri/target/release/bundle/flatpak/BrightTable_<version>_x86_64.flatpak
(Drop
--userfor a system-wide install instead, which needs root via polkit. Double-clicking the.flatpakfile in Files/GNOME Software does a system-wide install too, same end result.) -
Grant network access — required, not optional. The manifest's
finish-argsdoesn't include--share=network(onlybuild-options.build-argsdoes, and that only applies during theflatpak-builderbuild itself — it has zero effect on the installed app's runtime permissions). Without this grant, BrightTable can't reach your Immich server at all; you'll get connection errors the moment you try to configure a library in Preferences. Either:flatpak override --user --share=network io.github.brownphotographic.BrightTable
or in Flatseal: select BrightTable from the app list, then toggle Network on — it's one of the top-level switches on the app's permissions page, not nested under Filesystem/Session Bus/System Bus/Sockets.
-
Confirm it installed and check which kind:
flatpak info io.github.brownphotographic.BrightTable # shows "Installation: user" or "Installation: system" -
Launch it — either from your app menu as "BrightTable", or directly from a terminal:
flatpak run io.github.brownphotographic.BrightTable
-
If your photo library lives outside
$HOME(e.g. an NFS-mounted External Library), grant that path too — same CLI-override/Flatseal pattern as step 2, see "What this actually does under the hood" below for the exact command.
This is a local bundle install with no remote to track, so flatpak update
won't find a newer build on its own — reinstall over the old one instead (same --user/system choice as
above, matching however it's currently installed):
flatpak install --user --reinstall src-tauri/target/release/bundle/flatpak/BrightTable_<version>_x86_64.flatpakflatpak uninstall io.github.brownphotographic.BrightTableThe manifest lives at
flatpak/io.github.brownphotographic.BrightTable.yml.
Worth reading before you build, since it grants real permissions on your behalf:
--filesystem=homefor your photo library — deliberately not the broader--filesystem=host. If your library (or an NFS-mounted "External Library", per this app's own Immich pattern) lives outside$HOME, the sandbox can't see it until you grant that specific path yourself, either via Flatseal (Filesystem tab → add the path) or:flatpak override --user --filesystem=/path/to/your/library io.github.brownphotographic.BrightTable
--talk-name=org.freedesktop.Flatpak— lets the app launch host editors/CLI tools (ART, RawTherapee, exiftool, CUPS'slp/lpstat,xdg-open) viaflatpak-spawn --hostfrom inside the sandbox, since a sandboxed process can't exec a host binary directly (it doesn't share the host's library namespace). Seeapp/src-tauri/src/flatpak.rsfor the wrapper this backs.--filesystem=host-os:roplus an explicit read-only grant for Snap's desktop-export directory — so the app's editor auto-detection (which scans.desktopfiles under/usr/share/applicationsetc.) still finds natively-installed editors. Known limitation: even with this, editors installed somewhere unusual may not show up in the auto-detected list — use Preferences → Applications → "Other application…" to browse to it directly, which always works regardless of sandboxing.--talk-name=org.freedesktop.FileManager1and--system-talk-name=org.freedesktop.login1for "Show in File Manager" and the suspend-inhibit guard around long NFS operations, respectively — both cross the sandbox over D-Bus rather thanflatpak-spawn, so they just need the permission grant, nothing else.
Trade-off, stated plainly: the build uses --share=network to let cargo/npm resolve dependencies
live during the flatpak-builder build, rather than vendoring every dependency offline the way a Flathub
submission would require. Fine for building your own copy; would need real dependency vendoring
(flatpak-cargo-generator/flatpak-node-generator) before this could ever go on Flathub.
Compatibility
- Not sure if your Immich server version is supported? See COMPATIBILITY.md for which Immich version each BrightTable release has actually been tested against.
NFS Permission Errors (Immich + Unraid)
Seeing Permission denied errors when BrightTable writes metadata over NFS (e.g. Immich + Unraid)?
Symptom: BrightTable throws a permission error writing metadata (ratings, tags, etc.) to photos — either in an external library mounted into Immich, or in Immich's own managed upload library — even though the Docker mount shows rw, the Immich container runs as root, and the folder looks writable when checked directly on the Unraid host.
Root cause (two variants, both boiling down to a UID/GID mismatch over NFS):
- External library folders are typically owned by whatever UID/GID they were originally created under, with restrictive bits (e.g.
drwxr-xr-x) that don't grant write access to your NFS client user. - Immich-managed upload library folders are created by the Immich container itself, usually owned by Unraid's default
nobodyuser (uid 99, groupusers) with standard644/755permissions. Your client connects over NFS as a different UID (e.g.rbrown= uid 1000) — the kernel enforces permissions by numeric UID, not username, so the mismatch blocks the write even though both "look" like valid users.
Why ACLs don't fix it: setting a POSIX ACL (setfacl) on the Unraid host — even directly on the underlying ZFS dataset with posixacl enabled — doesn't reliably propagate over Unraid's shfs FUSE union export path (/mnt/user/...). getfacl from the NFS client shows no ACL entries at all, even though the ACL is present server-side. Go straight to chown/chmod instead.
The fix: apply a blunt permissions reset directly on the underlying disk path (not the /mnt/user/... shfs union path — find the real disk with ls -la /mnt/disk*/...):
chown -R nobody:users /mnt/bigdisk/Rob/Immich_Uploaded/library
chmod -R 777 /mnt/bigdisk/Rob/Immich_Uploaded/libraryPlain POSIX permission bits transmit reliably over NFS even though ACLs don't. Substitute the correct disk path for the external library if different.
Why it breaks again, and the permanent fix: a one-off chmod -R 777 only fixes files that exist at that moment — any file Immich creates afterward gets its default ownership/permissions again, so the error recurs on new photos. The official ghcr.io/immich-app/immich-server image doesn't support PUID/PGID/UMASK (that's a linuxserver.io convention, not used here), so there's no simple env-var fix. Instead, set up a recurring fix via Unraid's User Scripts plugin:
-
Install User Scripts from Community Applications, if not already installed.
-
User Scripts tab → Add New Script (e.g.
fix-immich-library-perms). -
Script contents:
#!/bin/bash chmod -R 777 /mnt/bigdisk/Rob/Immich_Uploaded/libraryAdd more
chmod -R 777 ...lines for any external library path(s) too. -
Schedule it (e.g. nightly, or a cron expression like
0 3 * * *), and optionally run it once manually to confirm it works immediately.
Higher-risk alternative, not recommended without testing: run the Immich container as your own UID (--user 1000:100 in the Unraid Docker template) so new files are owned correctly from creation. This avoids the recurring script but risks breaking permissions on /config (appdata) and other mounted paths that may currently be owned by root or a different UID.
If this recurs on a different library/folder later, work through in order: confirm the Docker mount is rw (docker inspect), confirm the container's UID (docker exec ... id), check ownership as Immich sees it (docker exec ... ls -la /path/inside/container), check for ACLs from the client (getfacl — if empty, skip straight to chown/chmod), find the real underlying disk path rather than fixing only /mnt/user/..., apply the chown/chmod fix above, and add it to the scheduled User Script if it's a folder Immich actively writes new files into.

