-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
Photosphere is a local-first, open-source media management application, the spiritual successor to Picasa, with a UI inspired by Google Photos. It works with photos and videos, with support for other media types planned for the future.
Photosphere is available as:
- A CLI tool (
psi) for building and managing databases from the command line - A desktop application (Electron-based, cross-platform, Windows, macOS, Linux)
- A mobile application (Android and iOS), coming soon
Note: some features are planned or maybe coming in the future. This document notes each of these.
flowchart TB
subgraph desktop["Desktop"]
d_app["Photosphere"]
d_db[("Local DB")]
end
subgraph mobile["Mobile"]
m_app["Photosphere"]
m_db[("Local DB")]
end
subgraph shared["Shared Storage (cloud or NAS)"]
db[("Full DB<br/>(encrypted)")]
end
d_app <--> d_db
m_app <--> m_db
d_app <-->|"sync"| db
m_app <-->|"sync"| db
Photosphere is built around data sovereignty: you own your files, you control where they live, and you decide who can access them. There is no Photosphere-operated backend, no account required, and no vendor lock-in. Cloud and remote storage are optional, supplementary, and automatically encrypted to maintain your privacy.
The foundational design concept is a Git-style database for large binary media files. Media files are treated as immutable binary objects, content-addressed by hash. Metadata is stored separately in a sharded BSON-format database with sort indexes baked to disk for very fast loading. This is the same content-addressing principle that Git uses to guarantee integrity, applied to photos and videos.
The CLI tool is aimed at technical users who want direct control over their databases. It provides commands for creating and managing databases, importing media, replicating, syncing, verifying integrity, repairing, and more, and is suited to scripting and automation.
See Command-Reference for a full list of commands.
The desktop app is Electron-based and cross-platform (Windows, macOS, Linux). It opens a media file database and provides a gallery UI for searching, viewing and editing media files.
Note: this is planned.
The mobile app is available for Android and iOS. It provides a gallery UI for searching, viewing and editing media files.
A Photosphere database is a directory in a file system, local, network (e.g. a NAS), or cloud. Databases are automatically encrypted by default; you must explicitly opt out if you do not want encryption.
The directory contains:
-
asset/, original media files -
display/, display-sized derivatives -
thumb/, thumbnails -
.db/, structured metadata: Merkle trees for integrity, a sharded BSON database of media file records, sort indexes baked to disk for fast GUI loading, and configuration
Every database has two UUIDs:
- Database UUID: shared across all replicas; used to verify that two databases are related before sync or replication
- Database instance UUID: (planned) unique to each individual database directory; used as the key under which the database's unique encryption key is automatically stored in the user's password manager
Databases can be full (all files present) or partial (only metadata and Merkle trees; media files, display images, and thumbnails are fetched lazily from the origin on first access).
The Merkle tree is the backbone of the database. Every file is hashed, and those hashes are combined up through a tree structure to produce a single root hash that represents the entire database state.
This design provides three key properties:
- Corruption detection: any unexpected change to any file changes the root hash, making corruption immediately detectable
- Efficient comparison: two databases can be compared by comparing root hashes; if they match, the databases are identical and no further work is needed; if they differ, the tree is traversed to locate exactly what has changed, which is vastly more efficient than scanning the file system
- Efficient sync and replication: only files that have actually changed are transferred, even for very large databases
Merkle trees are the data structure of choice in both Git and Bitcoin for the same reasons:
- Git represents every commit as a Merkle tree, each file is hashed, directory trees hash their children, and the commit hashes the root tree. Any change to any file produces a completely different root hash, making every repository state content-addressed and tamper-evident.
- Bitcoin stores the transactions in each block as a Merkle tree. This allows lightweight clients to verify that a specific transaction was included in a block using only a short proof (the path from the transaction up to the root), rather than downloading the full block.
flowchart TD
root["Root hash<br/>hash(H12 + H34)"]
h12["H12<br/>hash(H1 + H2)"]
h34["H34<br/>hash(H3 + H4)"]
h1["H1<br/>hash(file1)"]
h2["H2<br/>hash(file2)"]
h3["H3<br/>hash(file3)"]
h4["H4<br/>hash(file4)"]
f1["file1"]
f2["file2"]
f3["file3"]
f4["file4"]
root --> h12
root --> h34
h12 --> h1
h12 --> h2
h34 --> h3
h34 --> h4
h1 --> f1
h2 --> f2
h3 --> f3
h4 --> f4
Photosphere is entirely BYO storage, it never operates storage on your behalf. You point it at storage you already own or control:
| Backend | Format | Example |
|---|---|---|
| Local filesystem |
fs:path or bare path |
./my-photos |
| S3-compatible cloud | s3:bucket/path |
s3:my-bucket/photos |
Supported S3-compatible services include AWS S3, DigitalOcean Spaces, and MinIO. Additional backends (Dropbox, Google Drive, Azure) are planned.
Multiple devices share a database by pointing at the same shared storage location, a cloud bucket or NAS directory. No central broker is needed.
Note: password manager integration is planned; device keychain is implemented.
Photosphere never stores sensitive values in plain text on disk. Instead it uses two complementary secure storage mechanisms:
Device keychain: the operating system's secure credential store (e.g. macOS Keychain, Windows Credential Manager, Android Keystore). This is always used and works offline:
- User identity key: the private key representing the user's identity; must be available locally at all times
- Encryption keys for local databases: so the user can access their local data without an internet connection
Password manager: an optional integration (e.g. 1Password, Bitwarden) that syncs across devices. If no password manager is configured, all sensitive values fall back to the device keychain. When configured, it enables cross-device access without manual key transfer, and ensures your keys are not lost if a device is lost or replaced:
- Database encryption keys: stored under a key derived from the database instance UUID; opening the same database on a new device owned by the user automatically retrieves the right key
- Cloud storage credentials: S3 access keys, secrets, and endpoints so any device the user owns can access the same cloud database without manual configuration
- Google Maps API key: used for reverse geocoding of photo locations
- User identity key: stored in the password manager so it can be restored to new devices the user owns
Photosphere retrieves these values automatically when needed. On first use, if an encryption key does not yet exist, Photosphere generates it and stores it in the appropriate location. Through use of the password manager the user will rarely, if ever, need to copy or paste keys manually.
flowchart TD
app["Photosphere App"]
keychain["Device Keychain<br/>(always, offline)<br/><br/>• User identity key<br/>• Encryption keys for local databases"]
pm["Password Manager (optional, cross-device)<br/><br/>• Database encryption keys<br/>• Cloud storage credentials<br/>• Google Maps API key<br/>• User identity key"]
app -->|"reads/writes"| keychain
app -.->|"reads/writes<br/>(if configured and online)"| pm
Setting up a new device normally requires copying database configurations and secrets (S3 credentials, encryption keys, API keys) from an existing device. Photosphere makes this easy with built-in LAN sharing, a direct, encrypted transfer between two devices on the same local network, with no cloud relay or manual copying involved.
One device acts as the sender and the other as the receiver. The transfer is secured by four mechanisms working together:
- TLS encryption: the receiver generates a self-signed TLS certificate at runtime; all traffic is encrypted so nothing can be read in transit
- Certificate pinning: the certificate fingerprint is broadcast over the local network so the sender can verify it is connecting to the correct receiver, not an impersonator
- Mutual pairing code verification: the sender generates a 4-digit pairing code and displays it; the user enters it on the receiver. Both sides verify the other knows the code before any credentials are transmitted, the sender checks the receiver knows it before sending, and the receiver checks the sender knows it before accepting
- Rate limiting: the receiver enforces a strict limit on pairing attempts, preventing an attacker on the local network from brute-forcing the 4-digit code
flowchart LR
subgraph sender["Sender"]
s_app["Photosphere<br/>(shows pairing code)"]
end
subgraph receiver["Receiver"]
r_app["Photosphere<br/>(user enters pairing code)"]
end
s_app -->|"encrypted credentials<br/>(HTTPS over LAN)"| r_app
Both devices must be on the same local network (wired or Wi-Fi). This does not work over the internet.
See Sharing-Credentials for full details, including the security architecture.
Note: Currently opt-in, later this is planned to be more automatic.
Photosphere encrypts databases using a hybrid RSA-4096 + AES-256-CBC scheme. Each database has its own RSA key pair, the public key is used to encrypt data, and only the corresponding private key can decrypt it. The private key is stored in the device keychain and never leaves the device unencrypted.
Every file is encrypted independently with its own freshly generated random AES-256 key and IV, so each file's encryption is entirely independent. Data is encrypted client-side before it ever leaves your machine, cloud storage providers never see unencrypted content.
The combination of per-file random keys, a 256-bit key space, and Merkle tree integrity verification protects against brute-force, ciphertext correlation, key reuse, replay, and tampering attacks.
See Encryption for full technical details of the file format, key management, and CLI commands. See Security-Overview for the complete threat model, known limitations, and social engineering guidance.
Media files are imported into a Photosphere database by scanning source paths on the local file system and adding any new files found.
-
CLI:
psi addscans one or more paths and imports any new photos and videos it finds - Desktop app: imports from configured source folders on the local file system, running automatically in the background
- Mobile app: imports new photos and videos as they are taken or saved to the device, running automatically in the background
During import, each file is hashed, a thumbnail and display image are generated, metadata is extracted, and the file's record and Merkle tree are updated. Files already present in the database (matched by content hash) are skipped.
Failure safety: Photosphere guarantees that a failed or interrupted write, whether during import, replication, or sync, can never result in a corrupted or half-written file in the database:
- Local and network storage (e.g. NAS): every file is written to a temporary path first, then atomically renamed into its final location. A crash or interruption leaves only the temp file behind; the original file is never touched, and the temp file is simply discarded on the next run.
- Cloud storage (e.g. S3): uploads that do not complete successfully are never finalised and never become visible in the bucket. An interrupted upload simply disappears; it cannot be mistaken for a valid file.
In both cases, the Merkle tree is only updated after the file has been safely written, so the database is always consistent, any file recorded in the Merkle tree is guaranteed to be intact.
By design, files are always written to storage before the Merkle tree is updated. This means that if a process is interrupted between those two steps, the file exists in storage but has no record in the Merkle tree, an orphaned file. Orphaned files are harmless (they are never referenced) but do waste storage. Photosphere can detect and clean up files that are not accounted for in the Merkle tree.
Note: this is planned.
The desktop and mobile apps automatically import new photos and videos into your database in the background. On desktop, photos and videos are imported from the local file system. On mobile, new photos taken or saved to the device are imported automatically.
Once imported, photos and videos are automatically synced to your backup or sync database whenever you are online, securing them in encrypted cloud storage or on your NAS. You never need to manually back up; as long as the app is running and your shared storage is reachable, your media is protected.
flowchart TD
subgraph shared["Shared Storage (cloud or NAS)"]
s_db[("Full DB<br/>(encrypted)")]
end
subgraph device["Desktop / Mobile"]
app["Photosphere"]
local[("Local DB")]
media["New photos / videos"]
end
media -->|"auto import"| local
app <--> local
app <-->|"auto sync"| s_db
Each device running Photosphere maintains its own independent local database, typically a partial database (see Partial Databases below). Because every device has its own local copy, each can read and write freely without coordinating with other devices. There is no need for distributed locking or real-time communication between devices. This is also what makes Photosphere work fully offline, the local database is always available regardless of network connectivity, and any changes made while offline are synced the next time shared storage is reachable.
Devices stay in sync by reading and writing to a shared storage location, a cloud bucket or a NAS directory. No direct device-to-device connection is needed; the shared storage acts as the intermediary.
flowchart TD
subgraph device1["Device 1"]
d1_app["Photosphere"]
d1_db[("Local DB")]
end
subgraph shared["Shared Storage (cloud or NAS)"]
s_db[("Shared DB")]
end
subgraph device2["Device 2"]
d2_app["Photosphere"]
d2_db[("Local DB")]
end
d1_app <--> d1_db
d2_app <--> d2_db
d1_app <-->|"sync"| s_db
d2_app <-->|"sync"| s_db
Multiple processes can read a Photosphere database simultaneously, but writes must be serialised to prevent corruption. Photosphere uses a write lock stored at .db/write.lock in the database directory.
The write lock is acquired before any operation that modifies the Merkle tree or the BSON database, imports, syncs, and repairs all use it. If a lock is already held, the process retries a few times with increasing delays before failing. The lock records the owner session ID and the time it was acquired, so if a process crashes without releasing it, the stale lock can be identified and cleared.
This means it is safe to run multiple processes against the same database concurrently, for example, the desktop app and a psi command running at the same time. If the lock cannot be acquired after retrying, the operation fails cleanly rather than silently corrupting the database.
Photosphere is designed to work fully offline. The local database is always available regardless of network connectivity, so you can browse, search, view cached photos, import new media, and edit metadata at any time. Any changes made while offline are recorded in the local database and synced the next time shared storage is reachable.
What changes when you go offline:
| Offline | Online | |
|---|---|---|
| Browse and search | All indexed photos and videos visible as thumbnails | Full |
| View photos | Cached thumbnails and display images only | Full, including lazy-pull of uncached files |
| Uncached files | Not accessible, cannot be fetched without connectivity | Fetched lazily from origin on first access and cached locally |
| Import new media | Yes, written to local database | Yes |
| Edit metadata | Yes, written to local database | Yes |
| Sync | Not available | Automatic (desktop and mobile apps) |
| Lazy-pull of originals | Not available | On demand |
| Replication / backup | Not available | On demand or automatic |
| Storage quota trimming | Only files already synced to a full database can be trimmed | Files synced to a full database are eligible for trimming |
When connectivity is restored, the desktop and mobile apps resume syncing automatically. No user action is needed, changes made offline are merged into the shared database using the same field-level merge process as any other sync.
A consequence of the trimming rule is that photos and videos that originated on the device and have not yet been synced to a full database will never be trimmed while offline, they remain safely on the device until the sync completes. Only once a sync receipt confirms a file is durably stored in the full database can it be evicted from the partial database to free space.
psi replicate performs a one-way copy from a source database to a destination. The source and destination can each be a local path or an S3 location, any combination works. Replicas inherit the source database UUID, so they are recognized as the same database, but are allocated their own unique database instance UUID.
psi replicate --partial copies only structural metadata (Merkle trees, sort-index skeletons), not actual media files. The result is a lightweight partial database where files are fetched on demand.
An origin field in the database config records where a replica came from; this is used as the default source for subsequent sync, replicate, repair, and compare operations.
An initial full replication must copy all files and can take significant time depending on the size of the database and the speed of the storage. Subsequent replications use the Merkle tree to identify only what has changed, so they are very fast regardless of database size.
flowchart LR
subgraph device1["Device 1 (primary)"]
d1_db[("Full DB")]
end
subgraph shared["Shared Storage"]
s_db[("Replica DB")]
end
subgraph device2["Device 2"]
d2_db[("Partial DB")]
end
d1_db -->|"psi replicate"| s_db
s_db -->|"psi replicate --partial"| d2_db
psi sync performs a bidirectional sync between a local database and a remote one, bringing both sides fully up to date in one operation. The desktop and mobile apps sync automatically in the background when shared storage is reachable.
Sync runs in two phases, pull then push:
- Pull: files and records present in the remote but missing from the local database are copied in
- Push: files and records present locally but missing from the remote are copied out
The Merkle tree is used in both phases to identify exactly what has changed, so only differing files and records are transferred.
New files: detected by comparing the file-level Merkle trees. Any file present in one database but absent (or different) in the other is copied across.
Deleted files: deletions are recorded as a list of deleted file IDs in the Merkle tree metadata. When syncing, the deleted IDs from each side are propagated to the other: the corresponding media file, display image, and thumbnail are removed, and the BSON record is deleted.
BSON record merges: metadata records (photo titles, tags, ratings, etc.) are merged at the field level. Every field in a BSON record carries its own timestamp recording when it was last changed. When the same record exists in both databases, fields are merged by taking the version with the newer timestamp. Object fields are merged recursively. Array fields (e.g. a list of tags) are treated atomically, the array with the newer timestamp replaces the older one entirely.
Conflict handling: conflicts do occur when two devices edit the same field of the same record before syncing. Currently they are resolved automatically: the version with the newer timestamp wins; if both have the same timestamp, one is selected at random. Two devices editing different fields of the same record will both have their changes preserved, since each field is resolved independently. In the future, Photosphere may preserve all conflicting versions so the user can review them and choose which one to keep, with automatic last-write-wins as the default.
psi compare identifies differences between any two databases (local or cloud), using the Merkle tree to efficiently locate differences without scanning every file.
flowchart LR
subgraph db1["Database A (local or cloud)"]
a_db[("Database")]
end
subgraph db2["Database B (local or cloud)"]
b_db[("Database")]
end
compare["psi compare<br/>(Merkle tree diff)"]
result["Differences report"]
a_db --> compare
b_db --> compare
compare --> result
psi verify checks every file against its record in the Merkle tree. It checks file size and last-modified timestamp, if both match the recorded values, the file is considered unmodified without re-hashing. If either has changed, the content is re-hashed and the new hash is checked against the recoreded hash. The --full option forces content re-hashing of every file regardless of size or timestamp. Verify identities files that changed or become corrupted.
psi repair restores missing or corrupted files in a database. It detects missing or corrupt files (in the same way as psi verify). Missing files and corrupted files in the database are restored from the "source" database.
flowchart LR
subgraph target["Damaged database"]
t_db[("Database")]
end
subgraph source["Source (backup or origin)"]
s_db[("Database")]
end
verify1["psi verify<br/>(detect problems)"]
repair["psi repair<br/>(replace bad / missing files)"]
verify2["psi verify<br/>(confirm fixed)"]
t_db --> verify1
verify1 -->|"problems found"| repair
s_db --> repair
repair --> verify2
A partial database is important for two reasons: it is fast to create (only the core Merkle tree files are copied, so there is no waiting for media files to transfer), and it is small enough to fit on mobile devices without consuming large amounts of storage.
psi replicate --partial copies only the core database files, the Merkle trees. All other files (thumbnails, display images, originals, and BSON shard data) are pulled down lazily from the origin as they are needed.
From the user's perspective, a partial database is indistinguishable from a full one, lazy-pull is automatic and transparent:
- Thumbnails load as the gallery scrolls
- Display images load when a photo is opened
- Originals load on export
Storage quota: Note: this is planned. On mobile, Photosphere automatically derives a storage quota from the device's available storage. Once the quota is reached, locally cached files are evicted to make room for new ones, but only after a sync receipt confirms the file has been safely sync'd to a full database. This keeps the on-device database small enough to fit the device while ensuring no photo is deleted locally until it is durably backed up. The quota can optionally be configured manually by the user.
psi verify is aware of partial databases and does not report missing files as errors.
flowchart TD
subgraph origin["Origin (cloud or NAS)"]
full[("Full DB<br/>(all files)")]
end
subgraph device["Device"]
app["Photosphere"]
partial[("Partial DB<br/>(starts with minimal data)")]
end
full -->|"lazy-pull on first access<br/>(thumb → display → original)"| app
app -->|"cache locally"| partial
The database format evolves over time. Older databases must be migrated to the current format before use with psi upgrade, which rewrites the database in place.
Before migrating, create a backup with psi replicate in case anything goes wrong. Migration is a one-way operation, there is no downgrade path. The database UUID and all media files are preserved; only the internal structure changes.
Replicas do not need to be migrated separately. Once the source is migrated, a fresh psi replicate will bring replicas up to date.
flowchart LR
backup["psi replicate<br/>(back up first)"]
upgrade["psi upgrade<br/>(migrate in place)"]
verify["psi verify<br/>(check integrity)"]
replicate["psi replicate<br/>(update replicas)"]
backup --> upgrade --> verify --> replicate
Note: reverse geocoding currently runs during import. Deferring it to a separate phase after import is planned for the future.
When a photo or video has GPS coordinates in its metadata, it is tagged internally as pending reverse geocoding. Reverse geocoding converts raw GPS coordinates into a human-readable location (e.g. "Sydney, Australia"), which is stored in the file's record and is searchable in the gallery (e.g. .location=sydney).
Reverse geocoding is not done automatically on import, it is deferred:
-
CLI:
psi geocodeprocesses a batch of all photos and videos tagged as pending; only those not yet geocoded are processed - GUI: the desktop and mobile apps perform reverse geocoding automatically in the background, working through pending photos and videos without user intervention
Reverse geocoding requires a Google Maps API key, which is stored in the user's password manager and retrieved automatically when needed.
Note: this feature is planned.
Each user's identity is represented by a private key. On first use, if no identity exists, Photosphere generates one automatically and stores it in the device's keychain, and optionally in the user's password manager. On subsequent devices, the same identity key is retrieved from the password manager, or transferred directly from device to device via an encrypted URL payload or QR code. No Photosphere account or sign-up is required, identity is purely key-based and local.
If a user accidentally creates a new identity on a device instead of importing their existing one, Photosphere will provide a way to merge or reconcile the two identities, linking the new one back to the original so that history and access rights are preserved.
User identity unlocks a range of features that require knowing who is doing something:
- Comments and annotations: different users can comment on photos and galleries in a shared database; each comment is signed with the author's identity key so it can be attributed correctly
- Per-user data in shared databases: users can maintain their own partitioned data within a shared database (e.g. favourite photos) that is tied to their identity
- Encrypted messaging: encrypted messages can be addressed to a specific user (e.g. to securely share an encryption key or cloud storage credentials); only the holder of the recipient's private key can read them
- Device trust: a new device proves ownership by signing with the same identity key; this lets Photosphere automatically trust the device and grant it access to your databases without manual intervention
- Audit trail and provenance: database operations (imports, edits, deletions) can be signed with the user's identity key, creating a verifiable record of who changed what and when
- Access control: a database owner can grant a specific user access by encrypting the database key with that user's public key; the database itself records who has been granted access, with no central authority involved
- Sharing: photos or albums can be shared with a specific user by encrypting the content with their public key and dropping it into shared storage; only that user can decrypt it
All of these work purely through cryptography and the existing storage layer, no Photosphere-operated server or API is required.
Note: This is something that might never be implemented. Photosphere will operate without out.
Photosphere may in the future connect to an optional REST API. The apps do not depend on it and function fully without it, it would be supplementary, providing convenience features that are difficult or impractical to implement purely peer-to-peer.
Potential features include:
- Publishing and sharing: making photos or galleries publicly accessible via a URL, or sharing them with specific users without requiring direct device-to-device transfer
- Credential sharing: securely transferring storage credentials or encryption keys from one device to another, encrypted with the recipient's identity key, via the API as a relay
- Identity verification and management: a trusted third-party endpoint for confirming, recovering, or updating a user's identity key, particularly useful when reconciling accidentally duplicated identities
Whether this API will be implemented is undecided. It will never be required, Photosphere is and will remain a fully local-first application.
flowchart TD
subgraph devices["Devices"]
desktop["Desktop App"]
mobile["Mobile App"]
end
subgraph storage["Shared Storage"]
db[("Database<br/>(encrypted)")]
end
api["Photosphere REST API (optional):<br/>• Publishing / Sharing<br/>• Credential Relay<br/>• Identity Management"]
desktop <-->|"sync"| db
mobile <-->|"sync"| db
desktop <-.->|"optional"| api
mobile <-.->|"optional"| api
Photosphere ships with a built-in Model Context Protocol (MCP) server. MCP is an open standard that lets AI assistants such as Claude connect to external tools and data sources. Once connected, Claude can read your media database, search for photos, export files, and import new ones, all from the chat interface without leaving the conversation.
Two transports are available, suited to different scenarios:
| Transport | How it works | When to use |
|---|---|---|
| HTTP (desktop app) | Embedded server at http://localhost:3475/mcp, always running while the app is open |
Claude Desktop, Claude Code, or any HTTP-capable MCP client |
| stdio (CLI) |
psi mcp, spawned on demand by the MCP client |
Headless and server environments, or when the desktop app is not running |
When the desktop app starts, the Electron main process spawns a dedicated utility process running the MCP HTTP server. Isolating it in a utility process means a fault in the MCP layer cannot affect the main application.
The main process sends IPC messages to the worker to track which database is currently open. When the user opens a database in the GUI, the worker opens its own handle to it. When the user closes the database, the worker clears that handle; any tool call made after that point returns a "no database is currently open" error until a new database is opened.
The server uses the stateless per-request pattern from the MCP specification: each incoming POST to /mcp creates a fresh McpServer and StreamableHTTPServerTransport pair, handles the request, then tears them both down when the response closes. No per-session state is retained between requests.
sequenceDiagram
participant Claude as Claude (MCP client)
participant Main as Electron main process
participant Worker as MCP worker (utility process)
participant DB as Media database
Main->>Worker: start (port 3475)
Worker-->>Main: server-ready
Main->>Worker: database-opened (path)
Worker->>DB: open handle
Claude->>Worker: POST /mcp (tools/list)
Worker->>Worker: create McpServer + transport
Worker-->>Claude: tool list
Worker->>Worker: tear down
Claude->>Worker: POST /mcp (tools/call: search_media_files)
Worker->>Worker: create McpServer + transport
Worker->>DB: query
DB-->>Worker: results
Worker-->>Claude: tool result
Worker->>Worker: tear down
Main->>Worker: database-closed
Worker->>DB: close handle
Running psi mcp starts a stdio-based MCP server that reads JSON-RPC messages from stdin and writes responses to stdout. The MCP client (e.g. Claude Code) spawns this process when a session begins and shuts it down when it ends. The CLI server starts with no database open; Claude selects one at runtime using the list_databases and open_database tools.
The desktop (HTTP) server exposes the same tools as the CLI, plus a few more for modifying media files:
| Tool | What it does |
|---|---|
list_databases |
Lists the databases configured in this installation (from databases.toml). No parameters. |
open_database |
Opens a database by name or path; it becomes the active database for subsequent tools. |
close_database |
Closes the currently open database. No parameters. |
get_database_summary |
Returns a summary of the open database (file count, total size, hashes). No parameters. |
list_media_files |
Returns a page of media file summaries sorted by photo date, newest first. Accepts limit (1-200, default 20) and an optional pageId for paging. |
get_media_file_info |
Returns the full metadata record for one media file, given its asset ID. |
search_media_files |
Searches media files by a substring matched against filename and location, a content-type prefix (e.g. image or video/mp4), and/or an ISO 8601 date range. All filters are optional. |
save_media_file |
Saves a variant of a media file (original, display, or thumb) to a path on disk. Creates parent directories as needed. |
import_media_files |
Imports files or directories into the open database. With dryRun: true, reports what would be imported without writing anything. |
verify_database |
Runs a full integrity check over the open database and returns a summary of any issues. |
The desktop (HTTP) server adds three more: open_media_file, delete_media_file, and update_media_file.
See Claude-Integration for setup instructions and usage examples.
A full database lives in cloud storage (S3-compatible or other provider), encrypted. Desktop and mobile devices each hold a lightweight partial local database. Missing files are fetched lazily from the cloud origin as they are viewed. psi sync keeps cloud and local copies in sync.
flowchart TD
subgraph desktop["Desktop"]
d_app["Photosphere"]
d_db[("Partial DB<br/>(encrypted)")]
end
subgraph mobile["Mobile"]
m_app["Photosphere"]
m_db[("Partial DB<br/>(encrypted)")]
end
subgraph cloud["Cloud Storage"]
c_db[("Full DB<br/>(encrypted)")]
end
d_app <--> d_db
m_app <--> m_db
d_app <-->|sync| c_db
m_app <-->|sync| c_db
A full database lives on a NAS on the local network. Desktop and mobile devices replicate from the NAS (psi replicate --partial for a lightweight local copy) and sync back with psi sync. This setup stays entirely on-premises with no cloud dependency.
flowchart TD
subgraph desktop["Desktop"]
d_app["Photosphere"]
d_db[("Partial DB<br/>(encrypted)")]
end
subgraph mobile["Mobile"]
m_app["Photosphere"]
m_db[("Partial DB<br/>(encrypted)")]
end
subgraph nas["NAS box"]
n_db[("Full DB<br/>(encrypted)")]
end
d_app <--> d_db
m_app <--> m_db
d_app <-->|sync| n_db
m_app <-->|sync| n_db
A full database lives on a NAS for fast day-to-day access. A replicated copy is also pushed to cloud storage, encrypted with a separate key, for off-site backup. Desktop and mobile devices can work from the NAS when at home and from the cloud when away. Note that the NAS copy and the cloud copy each have their own unique encryption key; the desktop holds both.
flowchart TD
subgraph desktop["Desktop"]
d_app["Photosphere"]
d_db[("Partial DB<br/>(encrypted)")]
end
subgraph mobile["Mobile"]
m_app["Photosphere"]
m_db[("Partial DB<br/>(encrypted)")]
end
subgraph nas["NAS box"]
n_db[("Full DB<br/>(encrypted, NAS key)")]
end
subgraph cloud["Cloud Storage"]
c_db[("Full DB<br/>(encrypted, cloud key)")]
end
d_app <--> d_db
m_app <--> m_db
d_app <-->|"sync"| n_db
m_app <-->|"sync"| n_db
d_app <-->|"sync"| c_db
m_app <-->|"sync"| c_db
n_db -->|"sync"| c_db