# Backend Integration Contract This document is the frontend handoff contract for the `svg-service` backend. It is written as an integration baseline, not as an internal backend README. ## 1. Base URL and Auth - Base URL: `http://:9020` - API prefix: `/api/v1` - Auth header: `X-API-Key` All non-`/healthz` routes require an API key. Auth failure contract: - missing API key -> `401` with string detail: `Missing API key` - invalid API key -> `403` with string detail: `Invalid API key` - valid non-admin key on admin-only route -> `403` with string detail: `Admin role required` ## 2. Roles and Access Boundaries - `admin` - full access to protected routes - required for all `/api/v1/admin/...` routes - `operator` - allowed on non-admin protected routes - denied on admin-only routes - `viewer` - allowed on non-admin protected routes - denied on admin-only routes Frontend implication: - admin UI must treat admin routes as optional capabilities gated by role - frontend must not assume `operator` or `viewer` can call cleanup, audit, backfill, or current-artifact admin routes ## 3. Core Entities ### Upload Represents one uploaded SVG source and its normalized/sanitized artifacts. Important fields: - `upload_id` - `original_filename` - `content_type` - `size_bytes` - `original_storage_path` - `sanitized_storage_path` - `normalized_storage_path` - `normalized_elements_count` - `normalized_seats_count` - `normalized_groups_count` - `normalized_sectors_count` ### Scheme Top-level business object created from upload. Important fields: - `scheme_id` - `source_upload_id` - `name` - `status` - `current_version_number` - `published_at` ### Scheme Version Versioned snapshot of the scheme structure and publish state. Important fields: - `scheme_version_id` - `scheme_id` - `version_number` - `status` - `normalized_storage_path` - `normalized_*_count` ### Sector Structure entity in a specific `scheme_version`. Important fields: - `sector_record_id` - `sector_id` - `element_id` - `name` Business identity priority: - use `sector_id` when present - fallback to `element_id` - never treat `sector_record_id` as business identity across versions ### Group Important fields: - `group_record_id` - `group_id` - `element_id` - `name` Business identity priority: - use `group_id` when present - fallback to `element_id` - never treat `group_record_id` as business identity across versions ### Seat Important fields: - `seat_record_id` - `seat_id` - `element_id` - `sector_id` - `group_id` - `row_label` - `seat_number` Business identity priority: - use `seat_id` when present - fallback to `element_id` - never treat `seat_record_id` as business identity across versions ### Pricing Category Important fields: - `pricing_category_id` - `scheme_id` - `name` - `code` ### Price Rule Important fields: - `price_rule_id` - `scheme_id` - `pricing_category_id` - `target_type` - `target_ref` - `amount` - `currency` ### Artifact Artifact registry row for generated backend files. Important fields: - `artifact_id` - `artifact_type` - `artifact_variant` - `storage_path` - `status` - `meta_json` Important artifact types currently exercised by regression: - `sanitized_svg` - `normalized_json` - `display_svg` - `publish_preview` ## 4. Lifecycle State Machine ### Fresh Upload Flow: 1. `POST /api/v1/schemes/upload` 2. backend creates: - `upload` - `scheme` - initial `scheme_version` - structure rows - initial artifacts Expected initial state: - `scheme.status = draft` - `scheme.current_version_number = 1` - current version status = `draft` ### Current Draft If current scheme/version is still draft: - editor works directly against current version - `draft/ensure` is idempotent - `draft/ensure` returns `created=false` ### Ensure Draft From Published Current If current scheme/version is published: - `POST /api/v1/schemes/{scheme_id}/draft/ensure` - backend creates a new draft version - current pointer switches to the new draft - version number increments ### Publish Preconditions: - current scheme is draft - current version is draft - publish readiness must be satisfied Publish path: 1. optional `draft/pricing/snapshot` 2. `GET draft/publish-readiness` 3. optional `GET draft/publish-preview` 4. `POST /api/v1/schemes/{scheme_id}/publish` Expected result: - scheme becomes `published` - current version becomes `published` ### Rollback Path: - `POST /api/v1/schemes/{scheme_id}/rollback` Effect: - current pointer switches to requested historical `version_number` - scheme returns to `draft` - target version becomes current editable draft ### Unpublish Path: - `POST /api/v1/schemes/{scheme_id}/unpublish` Effect: - current scheme becomes `draft` - current version becomes `draft` ## 5. Editor Flow ### Entry Point - `GET /api/v1/schemes/{scheme_id}/editor/context` Use it first to decide whether: - current draft can be edited directly - or a new draft must be created from published current Important response fields: - `current_scheme_version_id` - `current_version_number` - `scheme_status` - `scheme_version_status` - `current_is_draft` - `create_draft_available` - `recommended_action` ### Draft Read Models - `POST /api/v1/schemes/{scheme_id}/draft/ensure` - `GET /api/v1/schemes/{scheme_id}/draft/summary` - `GET /api/v1/schemes/{scheme_id}/draft/structure` - `GET /api/v1/schemes/{scheme_id}/draft/validation` - `GET /api/v1/schemes/{scheme_id}/draft/compare-preview` Frontend should treat `draft/structure` as the main editable read model. ### Patch Operations Supported flows: - single seat patch - bulk seat patch - sector create/patch/delete - group create/patch/delete - repair references - remap preview/apply Frontend rule: - always send `expected_scheme_version_id` when mutating or reading draft state after editor entry ### Stale Conflict Handling If backend returns a stale or draft editability conflict: - stop optimistic local mutation flow - re-read: - `editor/context` - `draft/summary` - `draft/structure` Do not keep editing against stale cached `scheme_version_id`. ## 6. Pricing Flow ### Categories - `GET /api/v1/schemes/{scheme_id}/pricing` - `POST /api/v1/schemes/{scheme_id}/pricing/categories` - `PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}` - `DELETE /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}` ### Rules - `POST /api/v1/schemes/{scheme_id}/pricing/rules` - `PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}` - `DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}` ### Read Models - `GET /api/v1/schemes/{scheme_id}/pricing` - `GET /api/v1/schemes/{scheme_id}/pricing/coverage` - `GET /api/v1/schemes/{scheme_id}/pricing/unpriced-seats` - `GET /api/v1/schemes/{scheme_id}/pricing/explain/{seat_id}` - `GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics` - `GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price` - `GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}` Frontend rule: - empty pricing on a fresh upload is valid - do not treat `categories=[]` and `rules=[]` as backend failure ## 7. Publish Flow Main endpoints: - `POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot` - `GET /api/v1/schemes/{scheme_id}/draft/publish-readiness` - `GET /api/v1/schemes/{scheme_id}/draft/publish-preview` - `POST /api/v1/schemes/{scheme_id}/publish` Frontend sequencing rule: 1. ensure draft 2. mutate if needed 3. create/refresh pricing 4. build pricing snapshot 5. read publish readiness 6. read publish preview if UI needs preview surface 7. publish ## 8. Admin/Ops Flow Admin-only endpoints: - `GET /api/v1/admin/schemes/{scheme_id}/current/artifacts` - `GET /api/v1/admin/schemes/{scheme_id}/current/validation` - `POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate` - `POST /api/v1/admin/display/backfill` - `GET /api/v1/admin/artifacts/publish-preview/audit` - `POST /api/v1/admin/artifacts/publish-preview/cleanup` - `GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview` - `POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup` Healthy publish-preview audit contract: - `orphan_files_count = 0` - `missing_files_for_db_rows_count = 0` - `db_rows_count == disk_files_count` Frontend implication: - admin tools must not be shown as generally available functionality - admin cleanup/destructive flows must be role-gated on the client and still handle backend `403` ## 9. Typed Error Catalog ### Auth - `401` string detail: `Missing API key` - `403` string detail: `Invalid API key` - `403` string detail: `Admin role required` ### Lifecycle / Draft / Publish - `stale_draft_version` - `stale_current_version` - `current_version_inconsistent` - `draft_not_editable` - `publish_not_ready` ### Editor Uniqueness / References - `editor_uniqueness_error` - `editor_reference_error` - `duplicate_seat_id` - `duplicate_seat_id_in_payload` - `duplicate_sector_id` - `duplicate_group_id` - `duplicate_sector_element_id` - `duplicate_group_element_id` - `unknown_sector_id` - `unknown_group_id` - `unknown_sector_ids` - `unknown_group_ids` - `unknown_target_sector_id` - `unknown_target_group_id` - `business_identifier_nullification_forbidden` ### Pricing / Remap / Test - `invalid_amount` - `remap_filter_required` - `test_preview_failed` ### Validation Report Codes These appear inside validation report payloads rather than as top-level HTTP conflict codes: - `duplicate_seat_ids` - `missing_seat_contract` - `seats_without_sector_or_group` - `seats_without_price` Frontend rule: - do not parse only HTTP status - always inspect structured `detail.code` when `detail` is an object ## 10. Frontend Obligations - always handle auth failures `401` and `403` - always handle stale/conflict responses on draft, publish, and lifecycle operations - never treat `*_record_id` as stable cross-version business identity - always prefer business ids: - seat -> `seat_id`, fallback `element_id` - sector -> `sector_id`, fallback `element_id` - group -> `group_id`, fallback `element_id` - re-read current/draft state after: - any `409` - publish - rollback - unpublish - `draft/ensure` returning a newly created draft - do not assume current version remains stable across concurrent operator sessions - do not assume publish-preview artifacts or display artifacts are frontend-owned resources ## 11. Non-Persistent Assumptions Frontend Must Avoid The frontend must not assume that these remain stable forever: - `scheme_version_id` - `seat_record_id` - `sector_record_id` - `group_record_id` - artifact `storage_path` - publish-preview cache artifacts These are safe to treat as business-stable: - `scheme_id` - `version_number` within one scheme - `seat_id` when present - `sector_id` when present - `group_id` when present ## 12. Known Limitations / Deferred Tech Debt - some lifecycle negative contracts still return mixed styles: - typed object conflicts for `409` - plain string details for some `404` and auth cases - validation warnings and error code families are not yet unified into one single global error envelope - admin/ops routes are backend-internal tools, not end-user product APIs - corruption remediation smoke exists only for `publish_preview`, not for every artifact type ## 13. Regression Baseline Frontend Can Rely On The frontend can rely on the following regression-backed flows: - fresh upload on clean DB - current/draft/editor read flow - editor mutations and stale draft protection - pricing setup and publish flow - version lifecycle: - publish - ensure draft from published current - rollback - unpublish - admin ops: - audit - cleanup - destructive pricing cleanup for safe fixture categories - full admin permission matrix on implemented admin endpoints - controlled `publish_preview` corruption detection and remediation - negative upload validation - negative auth matrix - negative lifecycle matrix ## 14. Recommended Frontend Integration Sequence For normal editor work: 1. authenticate 2. upload or pick `scheme_id` 3. read `editor/context` 4. call `draft/ensure` if needed 5. read `draft/structure` 6. mutate using current `scheme_version_id` 7. on `409`, reload editor state before retry 8. configure pricing if needed 9. create pricing snapshot 10. read publish readiness / preview 11. publish For admin UI: 1. verify admin role in client auth state 2. call admin endpoints 3. still handle backend `403` 4. treat cleanup and remediation as explicit operator actions, not background automation