Sign in with Clerk and Photo Slop keeps your uploads, saved images and projects in a catalogue only you can read.

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.

KindHow it gets thereWhat is stored
Source (upload)Drop an image on the canvas, or Add images, while signed inThe 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
ProjectSave 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
ImageThe API only: export.render with dest: catalog and no documentA 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):

VariablePurpose
CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, CLERK_SIGN_IN_URLTurn 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_REDIRECTSatellite mode for a production host on *.2plot.dev
SESSION_SECRETSigns the session cookies; required in production
PHOTO_DEV_USERLocal 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:

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.

RouteWhat
GET /photo/api/catalog{ok, owner, profile, summary, items[]} — newest first
GET /photo/api/catalog/<id>One item
GET /photo/api/catalog/<id>/documentA 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=catalogSave 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: