Catalogue
The catalogue is where Photo Slop keeps what a signed-in user has made: the images they dropped into the editor, the renders they saved from it, and whole compositions ("projects") that can be opened again. It lives at /catalog and needs an account; signed-out visitors are shown the sign-in card.
What is kept
- Uploads — every image dropped on the canvas or added with "Add images" while signed in. The
original file is stored (converted to PNG or JPEG for the browser when it arrived as TIFF, HEIC or another format a browser cannot show), with a thumbnail for the catalogue grid.
- Projects — every save from the editor ("Save to catalogue" in the Catalogue tab, or any export
with "Keep in my catalogue" ticked) stores the PNG render AND the document JSON (the photo-canvas/1 document: artboard size, every layer with its position, text, effects and crop), the way an .excalidraw file travels beside its PNG. Download gives you the image; Open in editor loads the document so you pick up exactly where you left off (/editor?project=<id>).
- Images — a render without a document, which only the API produces (export.render with
dest: catalog and no document). It can be placed on the current canvas with /editor?add=<id>, as can an upload.
Anonymous visitors can still use the editor; nothing they make is kept between visits, and their uploads go to a shared, uncatalogued prefix.
Where it lives, and who can read it
Each user's files are stored under one key prefix in the asset backend, users/<owner-id>/…, together with a small index file that lists the items. The backend is whatever PHOTO_STORAGE_BACKEND selects — a local directory or a Cloudflare R2 bucket in production — so the catalogue needs no database.
Access is decided per file. A request for a key under users/<owner>/ is served only when the request's signed-in user is that owner; anyone else receives 404, so the existence of a file is not disclosed even to someone who guesses its URL. The identity comes from Clerk (dash-clerk-auth) and is read on the server for every request; the browser is never asked who it is. When the R2 backend is configured without a public domain (R2_PUBLIC_BASE unset) the app answers a file request with a short-lived signed URL after the ownership check; with a public domain set, bucket URLs are reachable by anyone who has them, so leave it unset for private catalogues.
The API behind the page
The page is a thin view over a small JSON API, also used by the editor's Catalogue tab:
- GET /photo/api/catalog — the signed-in user's items, profile and a storage summary.
- GET /photo/api/catalog/<id> — one item; GET /photo/api/catalog/<id>/document — a project's document.
- PATCH /photo/api/catalog/<id> with {"name": "…"} renames; DELETE removes the item and its files.
- Saving goes through the export route: POST /photo/api/export with dest=catalog (plus a name, and a
document field for a project). From the editor that is the export.render command with {"dest": "catalog", "name": "…", "project": true}.
Every endpoint answers 401 to a request with no signed-in user.
Limits
A catalogue holds up to 2000 items. The index is one file per user and is written last-writer-wins, so saving from two tabs in the same instant can lose one entry. Deleting an item deletes its files.
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /catalog/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt