Your photos are only read
PhotoBlad reads your photo folders and never changes them: nothing in them is moved, renamed, re-encoded or deleted, and nothing is written into them. That includes Import, which copies a folder’s photos and videos into PhotoBlad’s storage: it opens each file read-only, creates, renames and deletes nothing in the folder, and leaves it exactly as it was. In containers, Self-hosting has you mount your photo folders read-only, so the operating system itself keeps them untouched, and the doctor command warns about a folder mounted read-write.
PhotoBlad writes only in folders of its own, and scans always skip all of them:
- The data folder,
~/.photobladunless you setPhotoBlad:DataDirectory(in containers, thephotoblad-datavolume at/data), holds what PhotoBlad makes: its database, the thumbnails and previews, and videos’ playback copies. The database lists each photo’s file path and the details read from its EXIF data, including where it was taken when the photo has a GPS position. - The storage locations, the folders the owner chooses in Settings → Storage (
~/PhotoBlad/Uploadsto begin with, and/storage/mainin containers), hold the originals people back up, the photos and videos the owner imports, and versions. A storage location is never inside the data folder or a scanned folder, and a folder that holds scanned photos can’t become one.
A scan skips every storage location, however the path to its folder is written, a symbolic link included, and never records a file found inside one as a library photo, so a scan can’t touch a backup or turn it into part of the library.
Backups are written once
A backup is stored byte for byte. It arrives in a temporary .incoming folder in its storage location, is checked against its SHA-256 hash, saved to disk and made read-only, and only then moved to a name no other file has. After that PhotoBlad never changes or overwrites it: an original is only ever read, played, or downloaded exactly as it is. Imported photos and versions arrive the same way.
A file changes place only by a verified move, when someone is given a drive, a drive comes back after being offline, or a location is drained or over its cap. PhotoBlad copies the file into the other location, checks the copy’s SHA-256 hash against the recorded one, switches the library over to the copy, and only then removes the old file: exactly that file, inside its own location. A copy that fails its check leaves the old file where it was, and a move picks up where it stopped after a crash. Files in your scanned photo folders are never moved.
PhotoBlad writes into a storage location only after reading back its marker file, .photoblad-location. A drive that isn’t plugged in leaves an empty folder with no marker, so its location is offline and nothing is written there: PhotoBlad never writes into the empty folder a missing drive leaves behind.
Everything else is disposable. Thumbnails, video posters and playback copies are made from the originals, and made again if they’re lost. A playback copy leaves out the original’s metadata, such as where it was taken; the original keeps all of it.
Edits never touch your photos
Editing changes what you see, never the file. Crops, adjustments, filters and trims are stored in PhotoBlad’s database, and the edited look is made from the original, then kept in the data folder as disposable copies. Corrected dates, places, captions and tags stay in the database too, and nothing is written into a photo. Download original always gives the file exactly as it was.
- Who can edit. Owners edit the family library’s photos, and people edit their own backups. Everyone who can see a photo sees its edits, but no one is named as the editor.
- Hidden places stay hidden. When someone who may edit a photo removes where it was taken, nobody else is shown the camera’s location, not even in an edited download. The original file still holds it, as the editor says.
- Favourites are private. Your hearts are yours alone.
- Versions are written once. A version, such as a photo edited in another app, is stored like a backup: checked against its hash, read-only, and never changed. Deleting a version deletes only that file, never the original.
Backups are private
A backup is private to whoever made it. Nobody else sees it, the owner included: the owner role gets no exception.
- Sharing is each person’s choice. In Settings → Sharing, anyone can share their backups with the family. Everyone signed in then sees them, marked with the sharer’s name, and can download the originals. Turning sharing off hides them again straight away.
- Hidden means not there. Every list, thumbnail, video and download checks who is asking. A photo you can’t see gets the same “not found” as one that doesn’t exist.
- A file’s hash never grants access. Before uploading, a browser or phone asks which files the server still needs, by their SHA-256 hashes. A file only counts as backed up if you can already see it. Anything else is asked for, whether the server has never seen it or holds it for someone else, so the answer gives nothing away. To add a file someone else has backed up to your own backups, you have to send all of it, checked against its hash, and it’s still stored only once.
- The owner sees totals, not photos. Settings → Storage shows the owner how much each person has backed up, as counts and sizes only, and which drive keeps it.
- The library is everyone’s. The folders the owner scans are the family library, which everyone signed in sees, and so is anything the owner imports for the family library. That includes a backed-up photo that’s also in one of those folders, or in a folder imported for the family library. Only the owner sees where on the server the library is: its file paths, scan folders, import folders, storage locations and scan errors.
- Imports follow the same rules. What the owner imports for one person becomes that person’s backups, private until that person shares them.
Nothing leaves your machine
PhotoBlad is self-hosted: it runs on a computer you control, and your library, your backups, the database and the thumbnails stay on that computer.
- No requests to other sites. The web app loads everything from your own PhotoBlad, fonts included, and shows GPS positions as coordinates instead of calling a map service.
- No analytics or tracking. When you start PhotoBlad with Aspire, its services send their logs, traces and metrics to the Aspire dashboard on the same computer, and nowhere else. In containers the Compose files set up no such export, so the logs stay in the container runtime’s own log.
- The proxies talk to their own services. Caddy asks Let’s Encrypt for certificates for your domain, and the Tailscale container connects to Tailscale to join your tailnet and to get its certificate. Public certificate authorities such as Let’s Encrypt publish every certificate they issue in a public log, so the name in it, your domain or your node’s name on the tailnet, can be read by anyone. Neither service is given your photos or accounts: Caddy only asks for certificates, and what Tailscale carries between your devices is encrypted. On a home network, Caddy signs its own certificates, with no certificate authority outside.
Videos and FFmpeg
PhotoBlad never decodes a video itself. FFmpeg does, as a separate program on the same computer, and PhotoBlad reads back only the one small frame it takes for a poster. FFmpeg is optional: without it, videos are still backed up and listed. The container images include it.
FFmpeg is only ever given a local file, by its full path, and may read only local files in the video formats PhotoBlad supports, so a crafted video can’t make it read other files or reach the network. Each run has a time limit, and FFmpeg is stopped if it runs over. Frames larger than 100 megapixels are refused before FFmpeg decodes them.
Accounts and sign-in
Every page and every request needs a signed-in account, except the sign-in, first-run and invite pages.
- Accounts for your household. An owner and family members share the family library. The owner scans folders and manages members, and everyone can back up their own photos and videos.
- Passwords and passkeys. Sign in with a username and a password or a passkey. There’s no email address to give, and PhotoBlad never sends email. The owner makes a new sign-in link for a member who forgets their password; the owner’s own password is reset from the command line on the server.
- A setup code on first run. The first start prints a one-time code in the Server log, and the owner account can only be created with it, so nobody else can claim a new install.
- Single-use invite links. Each link works once, for up to 7 days, and making a new one cancels the old one. The link’s secret stays in the part of the address your browser never sends to the server.
- A session for each device. Every browser and phone signed in has its own session, kept on the server. A browser’s cookie holds only a random key, and a phone’s token is stored only as a hash. Settings → Devices lists your devices, and signing one out ends its session at once: its next request is refused.
- Hardening. Ten wrong passwords pause password sign-in for that account for five minutes; passkey sign-in isn’t affected. Sign-in attempts are rate limited. Session cookies can’t be read by scripts or sent by other sites, and requests from other sites are refused. Signing out all other devices, or changing your password, signs your other devices out at once, phones included, and making a member a new link or removing them signs out all of theirs; a password reset from the command line takes up to a minute. Pages are served with a strict Content Security Policy, and the Server and Indexer only trust each other through a shared key.
The data folder holds the keys that protect sign-in and the key the Server and Indexer share, as well as your library’s database. Keep it private, and include it in your backups. The storage locations hold everyone’s original backups, imported photos and versions: include every one too, hidden files and all, since a location’s marker file and manifest are what let PhotoBlad recover it after a lost database. In containers, the Caddy and Tailscale volumes hold private keys too, a certificate’s and the Tailscale node’s, so back them up as privately (see Backups and upgrades).
Reaching PhotoBlad from other devices
A copy you run from source answers only to localhost, 127.0.0.1 and [::1], so keep it on your own computer. To reach PhotoBlad from other devices, run it in containers as Self-hosting describes, behind a proxy that ends HTTPS: Caddy on your own domain, Tailscale on your own devices’ private network, or both. That setup keeps these promises:
- Only the proxy is published. The base Compose file publishes nothing. Caddy publishes its web ports, and the Tailscale container publishes none, so it is reached only through your tailnet. The one other way in is the Server published on
127.0.0.1, for trying PhotoBlad out, which only that computer can open. - The Indexer is never reachable from outside. It publishes no port, only the Server can reach it, and it answers only requests the Server signs with a key they share.
- PhotoBlad answers only at the names you give it. In containers they are the names in
PHOTOBLAD_HOSTNAMES, andlocalhost. Any other name gets400 Bad Request. - Only the proxy says who a client is. The Server believes the
X-Forwarded-ForandX-Forwarded-Protoheaders only from the private network the containers share, where the proxy is. From anywhere else they are ignored and logged, so no client can choose the address a rate limit counts, or make a request look like HTTPS. - HTTPS is the proxy’s job, and HSTS is Caddy’s. Caddy tells browsers to use HTTPS for your domain for a year, without
includeSubDomains, which would commit your other names, and withoutpreload. Tailscale’s route sends no such header, but nothing answers there on plain HTTP. - A passkey belongs to one host name. Passkeys work only at the exact https addresses you list in
PHOTOBLAD_PUBLIC_ORIGINS, and each belongs to the name it was made at, so register one for each name you use. Sessions are per name too. Passwords work at every name PhotoBlad answers at. - Nothing a proxy or a tailnet says signs anyone in. Tailscale tells a server who is connecting through headers. The Server never reads them, for any purpose, and a test fails if any of its source names one. PhotoBlad’s own accounts are the only identity.
- Tailscale Funnel is off. It would put PhotoBlad on the public internet, and a test fails if the Tailscale setup ever turns it on.
- The containers run with little. The Server and the Indexer run as an unprivileged user with a read-only root file system, every capability dropped and
no-new-privileges. - Caddy’s log leaves out what’s private. It keeps one line per request, without the query string (tags and searches are in it) and with the
Cookie,Set-CookieandAuthorizationheaders replaced byREDACTED. - Secrets stay private.
.envcan hold a setup code and a Tailscale auth key, so keep it out of Git; the repository ignores it.
This website
This site has no analytics or cookies and loads nothing from other sites; its fonts are served from here.
Reporting a security problem
[TBD: how to report a security problem privately]