Architecture overview
Cropwright is a thin frontend. It displays and sorts; the backend owns every decision (clustering, scoring, model training) and serves the result as data. See the architecture diagrams for the full picture.
Frontend
- SvelteKit 3 + Svelte 5 runes + TypeScript strict + Tailwind v4.
- Pointer-event drag-and-drop via
svelte-dnd-action(HTML5 drag-and-drop is unreliable across browsers/webviews and is not used). - State lives in Svelte 5 runes (
$state/$derived/$effect) — nothing inlocalStoragethat can't be reconstructed by an API call. - Built as a static site (SvelteKit's static adapter) and served by nginx in production.
One API prefix, one proxy
The browser only ever talks to Cropwright's own nginx origin. nginx proxies
everything under one path prefix (PUBLIC_API_PREFIX, default /curation)
to the OpenProcessor API container by Docker network name — no CORS, and no
backend hostname is ever exposed to the browser.
Browser --same-origin--> nginx (Cropwright, :8080 non-root) --docker network--> OpenProcessor API
Projects and the config store
One deployment hosts many projects. The active project is the URL
(/p/<project>/...), and every project-scoped request goes to that project's
own served prefix under the one API prefix. Switching project drops
everything cached for the previous one (classes, stores, undo history), and
any response that arrives late for the old project is discarded.
What a project is configured with, its prompt pack, region profile, active VLM endpoint and keymap, is stored by the backend as revisioned, activate-able documents ("the config store"); Cropwright is an editor and viewer for them. The VLM endpoint registry is shared by every project, with the active choice per project. See Runtime settings.
The served region profile
Cropwright ships with no domain built in. At most one "region" concept
(a sub-box within an item crop — a license plate on a vehicle, a tag on a
part, a defect zone) is active at a time, and it's entirely defined by what
the backend serves on GET {API_PREFIX}/health as region_profile. With no
profile served, every region-specific surface (a review tab, a gallery, a
sub-box editor) simply doesn't render — absent, not disabled.
A region item can carry any number of boxes; see Multi-box regions.
A deployment can add further, backend-unaware annotation slots via a small
JSON file (annotation-profiles.json) placed next to the built app. See
Annotation profiles.
Everything degrades gracefully
Every optional feature (score chips, the embedding-plot lasso tool,
semantic search, the region surfaces, the single-class export) is gated on
what the backend serves at runtime — its own /methods capability list or
the /health region profile — never a Cropwright build flag. A backend
that hasn't enabled a feature simply doesn't get a page section for it.
Absent, not disabled
Newer backend features (dataset import, prompt-pack and region-profile editors, the VLM registry, project combine, model sharing, the keymap) are each probed once and shown only when served, so one Cropwright build works against backends at different versions.
No database, no auth, of its own
Cropwright keeps no state beyond what the current page needs. It also implements no authentication — see Security before deploying anywhere but a trusted network.