Accounts & Catalogue
Sign in with Clerk and Photo Slop keeps your uploads, saved images and projects in a catalogue only you can read.
What an account gives you
The editor works for anyone. Signing in adds a catalogue at /catalog: everything you keep is stored with your account and served to your session alone.
| Kind | How it gets there | What is stored |
|---|---|---|
| Source (upload) | Drop an image on the canvas, or Add images, while signed in | The file (converted to PNG/JPEG for the browser when it arrived as TIFF, HEIC, …), the original when converted, a thumbnail. Sources are kept so your projects can reopen; they are counted in the summary but not shown as cards — a saved canvas is one card however many pictures it holds. /photo/api/catalog?kind=upload lists them |
| Project | Save to catalogue in the editor's Catalogue tab, or any export with Keep in my catalogue ticked (off by default for a download, ticked for you when Cloudflare R2 is the destination) | The PNG render and the document JSON — every layer with its position, text, effects and crop — the way an .excalidraw file travels beside its PNG. The render is what Download gives you; the document is what Open in editor loads |
| Image | The API only: export.render with dest: catalog and no document | A render without a document, a thumbnail |
From the catalogue page, Download fetches the item's image as a file through the app (/photo/api/catalog/<id>/file, owner-checked), Open in editor loads a project so you pick up exactly where you left off (/editor?project=<id>), and Add to canvas places an upload or image on the current canvas (/editor?add=<id>). The editor's own Catalogue tab lists your most recent items with the same actions. Delete removes the item and its files.
An export to Cloudflare R2 (or, through the API, dest=disk) also writes the document beside the image, as <same name>.json in the same folder, so a file you find in a bucket or a directory a year from now can still be opened: document.load takes it as-is.
Signing in
Authentication is Clerk through dash-clerk-auth, exactly as the 2plot network's other sites use it: the site is a Clerk satellite of 2plot.dev, so one account signs you into every site in the network. The header shows the sign-in control when Clerk is configured; the catalogue page shows a sign-in card to signed-out visitors.
Server configuration, all in the environment (see .env.example):
| Variable | Purpose |
|---|---|
CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, CLERK_SIGN_IN_URL | Turn Clerk on. With any of them unset the site runs without accounts and the catalogue is unavailable |
CLERK_FRONTEND_API, CLERK_IS_SATELLITE, CLERK_SATELLITE_DOMAIN, CLERK_SIGN_UP_URL, CLERK_SATELLITE_SIGN_IN_REDIRECT | Satellite mode for a production host on *.2plot.dev |
SESSION_SECRET | Signs the session cookies; required in production |
PHOTO_DEV_USER | Local work only. A stand-in owner so the catalogue can be used without Clerk keys. Ignored when Clerk is on and ignored in production (RENDER or APP_ENV=production) |
Where your files live
Every signed-in user owns one key prefix in the asset backend (Storage & Export):
users/<owner>/catalog.json the index of this user's items
users/<owner>/assets/<yyyy>/<mm>/<id>.jpg uploads (+ .thumb.webp, + .orig.heic when converted)
users/<owner>/exports/<yyyy>/<mm>/<id>.png saved images
users/<owner>/projects/<yyyy>/<mm>/<id>.png saved projects (+ <id>.json, the document)
The index is a small JSON file kept in the same backend as the bytes, so the catalogue works identically on a local directory and on Cloudflare R2 and needs no database. The trade-off: it is written last-writer-wins per user, so two tabs saving in the same instant can lose one entry. A catalogue holds up to 2000 items.
The owner id is derived from the Clerk user id (made key-safe: user_2abc… becomes user-2abc… plus a short fingerprint), never from an email address, so a changed email never orphans a catalogue.
Who can read what
Access is decided per file, on the server, every time:
- A request for
/photo/files/users/<owner>/…is served only when the request's signed-in user is that owner. Anyone else — signed out, or another user — receives 404, so the existence of a file is not disclosed to someone who guesses a URL. - Your files are always addressed through the app (
/photo/files/users/…), on every backend — even R2 with a public domain configured — and the app streams the bytes after the owner check. That is what lets the editor draw them from any origin without a CORS rule on the bucket. Public (signed-out) uploads andexports/keep the bucket's URL. - On the R2 backend the same check runs before the app issues a short-lived signed URL. Set
R2_PUBLIC_BASEonly for a bucket you mean to be public: with it set, URLs point straight at the bucket and bypass the app. - Anonymous uploads go to the shared
assets/prefix, are catalogued nowhere, and are readable by anyone with the (random) URL, as before. - The browser is never asked who it is. Identity is the Clerk session cookie, verified by
dash-clerk-authon each request, on Flask and FastAPI alike.
Browsers cache a user's own files privately (Cache-Control: private); shared caches never see them.
The API
The catalogue page and the editor's tab are thin views over a small JSON API. Every route answers 401 without a signed-in user.
| Route | What |
|---|---|
GET /photo/api/catalog | {ok, owner, profile, summary, items[]} — newest first |
GET /photo/api/catalog/<id> | One item |
GET /photo/api/catalog/<id>/document | A project's document JSON |
PATCH /photo/api/catalog/<id> with {"name": "…"} | Rename |
DELETE /photo/api/catalog/<id> | Remove the item and its files |
POST /photo/api/export with dest=catalog | Save a render; add name, and document (the JSON) for a project |
From the editor, saving is the ordinary export.render command:
{ "type": "export.render", "payload": { "format": "png", "dest": "catalog", "name": "Summer", "project": true } }
The acknowledgement's server object carries the new item's id, kind, url and thumbUrl. An agent with a signed-in browser tab can therefore save its work to the user's catalogue like any other export.
Lifespans
Everything in a catalogue expires — 30 days after it was saved for a member, 90 for the site owner (PHOTO_CATALOG_TTL_DAYS / PHOTO_CATALOG_TTL_OWNER_DAYS; 0 switches it off). Each card says when it goes, the header says so too, and expired items are removed with their files the next time the catalogue is read, by the hourly sweep run.py starts, or by python3 scripts/catalog_purge.py. Download what you want to keep. The rail exists so an abandoned account cannot fill the bucket for ever; the full set of rails — the spend cap, the owner-only model, the storage accounting — is on Usage, limits & costs.
Running it locally
PHOTO_DEV_USER=me PHOTO_STORAGE_BACKEND=disk python run.py
Open /editor, drop an image, and it appears under /catalog. Switch PHOTO_DEV_USER to another name and the catalogue is empty: each stand-in is its own owner, which is also how the browser test lane (tests/test_photo_browser.py) exercises the isolation.
Source: /guide/catalog
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:
- /guide/catalog/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt