Keepsake is your own private photo library — a place to upload, browse, and rediscover your photos without handing them to a big-tech cloud. It runs entirely in your Azure subscription, so your memories stay yours.
Once your photos are in, Keepsake does the tedious work for you: it makes fast-loading previews, reads text and locations off your pictures, tags what's in them, recognizes the people who show up again and again, and lets you search your whole library in plain language. You organize the rest into albums — by hand or automatically — and share them with a link.
It's open source and self-hostable, licensed under AGPL-3.0.
Much of the heavy lifting (thumbnails, text extraction, tagging, face detection) happens right in your browser, so your photos aren't shipped off to a third-party AI service to be understood.
The gallery is the home for every photo and video you've uploaded. From here you can:
Albums are collections you curate on top of your library. On the Albums page you can:
The Tools page is the behind-the-scenes view of everything Keepsake does to each photo after upload. Every photo runs through a set of processing stages:
For each photo you can see the status of every stage at a glance, and filter to find work that still needs doing — e.g. photos that failed a stage or that have no data for a particular process (thumbnail, EXIF, OCR, AI vision, map, or face). It's where you go to check that processing is complete or to chase down anything that got stuck. Related: the Corrupted uploads view surfaces files that couldn't be processed at all.
Keepsake detects faces and clusters photos of the same person together. On the People page you can:
All face detection and clustering runs in your browser, so faces are recognized without sending your photos to an outside service.
This provisions the whole app into your own Azure subscription from prebuilt
public container images — no build step required. The template lives at
deploy/ (main.bicep → compiled azuredeploy.json).
The template deploys at subscription scope and creates its own resource
group (<appName>-rg by default), so you're only asked for a subscription,
a region, and a few details — no need to pick or create a resource group first.
OCR, face detection, vision tagging, and geo/reverse-geocoding can each run
in your browser (the default), on the server, or both. Pick this
in the deploy form's "Photo processing" dropdown (processingMode parameter):
| Mode | What happens |
|---|---|
| In your browser (default) | Same as always — everything runs client-side, free, no extra Azure cost. Requires a reasonably capable device; nothing runs until you click "Load browser AI." |
| On the server | The browser skips this work entirely. A new ipworker container app processes every upload instead — better for low-power/mobile devices, and it's the only mode that lets you bulk-reprocess an existing library from the Tools page (see below) without a browser tab open. Costs more to run (ipworker needs meaningfully more CPU/memory than the rest of the deployment) and only scales up when there's work queued. |
| Both | Browser and server both attempt each step; whichever finishes first for a given photo wins, the other is discarded. Roughly doubles compute cost per step — useful mainly for comparing the two paths, not a typical choice. |
ipworker pulls a separate public image, ghcr.io/git4rajat/photostore-ipworker:latest
(also gated behind the Publish images workflow
being run and set to Public in GHCR, same as the backend/frontend images
above). It's only deployed at all when you choose "On the server" or "Both" —
in the default browser mode, no ipworker container app is created.
You can change this later by re-running the deployment with a different
processingMode value (Azure Portal → your resource group → Deployments →
Redeploy, or az deployment group create with the updated parameter).
In the deploy form you set a login email and password. That's your sign-in — no Microsoft account, app registrations, or admin consent required. When the deployment finishes, open the app URL and log in.
owner-password
secret on the <appName>-backend Container App in the Azure Portal.OWNER_EMAIL)
in the deploy form — password-mode sign-in is by email + password, so an empty
email leaves the owner unable to log in. If you deployed without one, set
OWNER_EMAIL on the <appName>-backend Container App and restart; the account
reconciles to that address on the next boot.From the Sharing tab you can invite other people by email. Choose whether they join your library (they see the same photos, as an equal member — up to 15 people including you) or start their own fresh library. Invitees get an email link (valid 72 hours) to set a password and sign in. Only the library owner can invite or remove members; any member can leave, and switching between libraries you belong to happens from the same tab. (Sharing requires the reset email transport above to be configured, since invites are delivered by email.)
Advanced: Microsoft Entra SSO. Prefer enterprise single sign-on instead of a password? Deploy, then run
deploy/setup-auth.shin Azure Cloud Shell to create Entra app registrations and switch the app toAUTH_MODE=entra. This requires rights to register apps and grant admin consent in your directory.
Note: the button pulls the public images
ghcr.io/git4rajat/photostore-backend:latestand-frontend:latest. These must be published (via the Publish images workflow) and set to Public in GHCR before a deploy can succeed.
This is a quick, task-by-task guide to getting the most out of Keepsake once you're signed in.
Processing (thumbnails, text, tags, faces, location) keeps running after upload — you don't have to wait on it. Check progress any time on the Tools page.
You'll see the same icons throughout the gallery and on individual photos:
ipworker is
actively working on that photo; disappears once it finishes.Use the filter controls at the top of the Gallery to narrow things down:
Filters combine, so you can, for example, show only liked videos rated 4+.
The People page groups faces that belong to the same person into a cluster of face crops. From here you can:
Whether two face crops end up in the same person cluster is controlled by a
preset on the backend (PEOPLE_CLUSTER_PRESET env var, default strictest):
| Preset | Same-person link threshold | Behavior |
|---|---|---|
strictest (default) | cosine similarity ≥ 0.80 | Only very confident matches merge — favors leaving similar-looking people as separate clusters over wrongly merging them. |
strict | ≥ 0.76 | Slightly looser than strictest, still errs toward false negatives. |
balanced | ≥ 0.72 | Middle ground — this is the setting that best matched real calibration data in testing. |
loose | ≥ 0.66 | Merges more readily, tolerating more variation in lighting/angle, at higher risk of fusing two different people. |
Switching preset (e.g. to balanced for more auto-merging) is just an env var
change on the backend and worker — no code change or redeploy of new code
needed. See _resolve_people_cluster_config in backend/app.py for the full
threshold table and the calibration script
(backend/scripts/calibrate_face_thresholds.py) used to derive these numbers
from real face-embedding data.
The Tools page shows every processing stage each photo goes through:
For each photo you can see the status of every stage at a glance. Use the filters to find work that still needs doing — for example, photos that failed a stage or have no data for a given process.
Re-running a stage: if a photo failed or gave a bad result (a missing tag, a face that wasn't detected, wrong location), filter to the affected photos and re-run that processing stage on them. This kicks off the AI action again for those photos without touching anything else. The Corrupted uploads view lists files that couldn't be processed at all.
If you deployed with "On the server" or "Both" processing (see Choosing
where AI processing runs), re-running a
stage here also queues it to ipworker — this is how you bulk-reprocess an
existing library server-side, with no browser tab needing to stay open.
Albums are collections you build on top of your library.
Create an album by hand:
Create a smart album (Keepsake fills it automatically from a rule) — on the Albums page choose New smart album and pick a rule:
Add or remove photos in an existing album:
Search within an album by name, minimum rating, or liked-only to find a specific shot fast, and download the whole album (or a selection) as a batch.
To stop sharing, open the album's share settings and revoke the link.
You can change your password any time from inside the app (see your account settings). Forgot it? Use Forgot password? on the login screen to get a reset link by email — check your spam folder if it doesn't arrive.
220 commits
Python
46.3%
TypeScript
41.5%
JavaScript
5.0%
CSS
4.5%
Bicep
1.7%
Keepsake is your own private photo library — a place to upload, browse, and rediscover your photos without handing them to a big-tech cloud. It runs entirely in your Azure subscription, so your memories stay yours.
Once your photos are in, Keepsake does the tedious work for you: it makes fast-loading previews, reads text and locations off your pictures, tags what's in them, recognizes the people who show up again and again, and lets you search your whole library in plain language. You organize the rest into albums — by hand or automatically — and share them with a link.
It's open source and self-hostable, licensed under AGPL-3.0.
Much of the heavy lifting (thumbnails, text extraction, tagging, face detection) happens right in your browser, so your photos aren't shipped off to a third-party AI service to be understood.
The gallery is the home for every photo and video you've uploaded. From here you can:
Albums are collections you curate on top of your library. On the Albums page you can:
The Tools page is the behind-the-scenes view of everything Keepsake does to each photo after upload. Every photo runs through a set of processing stages:
For each photo you can see the status of every stage at a glance, and filter to find work that still needs doing — e.g. photos that failed a stage or that have no data for a particular process (thumbnail, EXIF, OCR, AI vision, map, or face). It's where you go to check that processing is complete or to chase down anything that got stuck. Related: the Corrupted uploads view surfaces files that couldn't be processed at all.
Keepsake detects faces and clusters photos of the same person together. On the People page you can:
All face detection and clustering runs in your browser, so faces are recognized without sending your photos to an outside service.
This provisions the whole app into your own Azure subscription from prebuilt
public container images — no build step required. The template lives at
deploy/ (main.bicep → compiled azuredeploy.json).
The template deploys at subscription scope and creates its own resource
group (<appName>-rg by default), so you're only asked for a subscription,
a region, and a few details — no need to pick or create a resource group first.
OCR, face detection, vision tagging, and geo/reverse-geocoding can each run
in your browser (the default), on the server, or both. Pick this
in the deploy form's "Photo processing" dropdown (processingMode parameter):
| Mode | What happens |
|---|---|
| In your browser (default) | Same as always — everything runs client-side, free, no extra Azure cost. Requires a reasonably capable device; nothing runs until you click "Load browser AI." |
| On the server | The browser skips this work entirely. A new ipworker container app processes every upload instead — better for low-power/mobile devices, and it's the only mode that lets you bulk-reprocess an existing library from the Tools page (see below) without a browser tab open. Costs more to run (ipworker needs meaningfully more CPU/memory than the rest of the deployment) and only scales up when there's work queued. |
| Both | Browser and server both attempt each step; whichever finishes first for a given photo wins, the other is discarded. Roughly doubles compute cost per step — useful mainly for comparing the two paths, not a typical choice. |
ipworker pulls a separate public image, ghcr.io/git4rajat/photostore-ipworker:latest
(also gated behind the Publish images workflow
being run and set to Public in GHCR, same as the backend/frontend images
above). It's only deployed at all when you choose "On the server" or "Both" —
in the default browser mode, no ipworker container app is created.
You can change this later by re-running the deployment with a different
processingMode value (Azure Portal → your resource group → Deployments →
Redeploy, or az deployment group create with the updated parameter).
In the deploy form you set a login email and password. That's your sign-in — no Microsoft account, app registrations, or admin consent required. When the deployment finishes, open the app URL and log in.
owner-password
secret on the <appName>-backend Container App in the Azure Portal.OWNER_EMAIL)
in the deploy form — password-mode sign-in is by email + password, so an empty
email leaves the owner unable to log in. If you deployed without one, set
OWNER_EMAIL on the <appName>-backend Container App and restart; the account
reconciles to that address on the next boot.From the Sharing tab you can invite other people by email. Choose whether they join your library (they see the same photos, as an equal member — up to 15 people including you) or start their own fresh library. Invitees get an email link (valid 72 hours) to set a password and sign in. Only the library owner can invite or remove members; any member can leave, and switching between libraries you belong to happens from the same tab. (Sharing requires the reset email transport above to be configured, since invites are delivered by email.)
Advanced: Microsoft Entra SSO. Prefer enterprise single sign-on instead of a password? Deploy, then run
deploy/setup-auth.shin Azure Cloud Shell to create Entra app registrations and switch the app toAUTH_MODE=entra. This requires rights to register apps and grant admin consent in your directory.
Note: the button pulls the public images
ghcr.io/git4rajat/photostore-backend:latestand-frontend:latest. These must be published (via the Publish images workflow) and set to Public in GHCR before a deploy can succeed.
This is a quick, task-by-task guide to getting the most out of Keepsake once you're signed in.
Processing (thumbnails, text, tags, faces, location) keeps running after upload — you don't have to wait on it. Check progress any time on the Tools page.
You'll see the same icons throughout the gallery and on individual photos:
ipworker is
actively working on that photo; disappears once it finishes.Use the filter controls at the top of the Gallery to narrow things down:
Filters combine, so you can, for example, show only liked videos rated 4+.
The People page groups faces that belong to the same person into a cluster of face crops. From here you can:
Whether two face crops end up in the same person cluster is controlled by a
preset on the backend (PEOPLE_CLUSTER_PRESET env var, default strictest):
| Preset | Same-person link threshold | Behavior |
|---|---|---|
strictest (default) | cosine similarity ≥ 0.80 | Only very confident matches merge — favors leaving similar-looking people as separate clusters over wrongly merging them. |
strict | ≥ 0.76 | Slightly looser than strictest, still errs toward false negatives. |
balanced | ≥ 0.72 | Middle ground — this is the setting that best matched real calibration data in testing. |
loose | ≥ 0.66 | Merges more readily, tolerating more variation in lighting/angle, at higher risk of fusing two different people. |
Switching preset (e.g. to balanced for more auto-merging) is just an env var
change on the backend and worker — no code change or redeploy of new code
needed. See _resolve_people_cluster_config in backend/app.py for the full
threshold table and the calibration script
(backend/scripts/calibrate_face_thresholds.py) used to derive these numbers
from real face-embedding data.
The Tools page shows every processing stage each photo goes through:
For each photo you can see the status of every stage at a glance. Use the filters to find work that still needs doing — for example, photos that failed a stage or have no data for a given process.
Re-running a stage: if a photo failed or gave a bad result (a missing tag, a face that wasn't detected, wrong location), filter to the affected photos and re-run that processing stage on them. This kicks off the AI action again for those photos without touching anything else. The Corrupted uploads view lists files that couldn't be processed at all.
If you deployed with "On the server" or "Both" processing (see Choosing
where AI processing runs), re-running a
stage here also queues it to ipworker — this is how you bulk-reprocess an
existing library server-side, with no browser tab needing to stay open.
Albums are collections you build on top of your library.
Create an album by hand:
Create a smart album (Keepsake fills it automatically from a rule) — on the Albums page choose New smart album and pick a rule:
Add or remove photos in an existing album:
Search within an album by name, minimum rating, or liked-only to find a specific shot fast, and download the whole album (or a selection) as a batch.
To stop sharing, open the album's share settings and revoke the link.
You can change your password any time from inside the app (see your account settings). Forgot it? Use Forgot password? on the login screen to get a reset link by email — check your spam folder if it doesn't arrive.
220 commits
Python
46.3%
TypeScript
41.5%
JavaScript
5.0%
CSS
4.5%
Bicep
1.7%