Backend API
Facts verified 2026-08-10 by claude-code/2026.08. Generated from the authored source at docs/llm-wiki/backend-api.md — the wiki owns these facts (ADR: D1 of docs/plans/20260811_documentation_system_execution.md). Edit that file, then regenerate: python docs/portfolio/_build/generate_wiki_pages.py.
Shape
- The web app’s own backend (backend-for-frontend): 104 route handlers / 125 operations under
web/app/api/, serving JSON to the browser for everything that is not AI chat. - Spec:
web/public/api-docs/swagger.yaml— hand-maintained OpenAPI 3.0, version 1.7.0, complete and drift-free as of 2026-08-10;scripts/check_swagger_drift.py(run in CI) diffs the spec against the route files, checks declared body fields against the handler’s destructuring, and verifies eachsecuritydeclaration is true of its handler — failing an operation that namesSupabaseAuthwith no path reading a bearer, or one requiring auth whose handler checks nothing. - Every operation has a unique
operationId; 16 carryx-agent-tool(the AI-callable surface, all bearer-reachable) and 8 carryx-environment: development(the/debug/*family andtest-image-visualization, which return 404 whenNODE_ENV=production). - CLI:
kavai apigenerates one command per operation from this spec (ai/src/kavai/cli/api/, index inai/src/kavai/web_api/).kavai api coverage --probecalls the reads and reports wheresecurityand the backend disagree. Skill:.claude/skills/kap-api-cli/. - Interactive Swagger UI:
/api-docsin the running app (web/app/api-docs/page.tsx); raw spec served at/api-docs/swagger.yaml. - Spec paths are relative to
servers: /api(spec/datasets=/api/datasets). - Plain-English list of all 119 operations:
docs/handbooks/content/web/api-inventory.md.
Authentication
- Two schemes:
CookieAuth(Supabase session cookie, viacreateServerClientfromweb/lib/supabase/server.ts) andSupabaseAuth(bearer JWT). As of 2026-08-10: 88 operations accept a bearer, 23 are cookie-only, 11 are public — a bearer is the normal case now, not the exception, because AI engines hold one. requireUser()inweb/lib/api-auth.tsis the shared session check (cookie first, bearer JWT alternative); new routes should use it. 401 body is{ "error": "Unauthorized" }. On success it also returnssupabase— a client scoped to the caller’s identity, so queries through it are RLS-enforced; use it for org-scoped reads and access probes instead of the service-role client wherever a policy exists. Auth tests:web/tests/api/route-auth.test.ts,web/tests/api/route-org-scoping.test.ts.- Errors are always JSON
{ "error": "…" }(the spec’sErrorschema).
Security worklist — cleared 2026-07-31, and what 2026-08-10 found after it
The former ⚠ worklist (privileged service-role routes with no session check) was closed out on 2026-07-31 in two steps: all 21 flagged operations require a session via requireUser() (401 without one), and the org-scoping gap is fixed — reads go through the caller’s RLS-scoped client (auth.supabase), writes are gated on an RLS access probe first (404/403 out of scope), /api/datasets/create inserts under the RLS INSERT policy, and /api/gas-readings reads the security-invoker v_dataset_gas_readings view. /api/organizations/create-for-user is now self-service only (userId must equal the caller; 403 otherwise) — the app has no platform-admin role, so a cross-user version would need one first. The /api/aps/* family is now org-scoped. /api/aps/auth hands the browser a viewables:read-only token (getViewerToken() in web/app/api/aps/_lib/aps-auth.ts). Uploads go to a per-org OSS bucket (kavai_cad_org_<orgId>; web/app/api/aps/_lib/buckets.ts) — /api/aps/upload requires a validated organizationId the caller is a member of, and /api/aps/translate + /api/aps/status decode the URN’s bucket and 403 anything outside the caller’s org (the shared demo bucket kavai_cad_demo is readable by all). Accepted residual: the browser viewer token is app-wide viewables:read, so a user who already knows another org’s exact URN can still view (not download or modify, and not translate/poll via our API) that model directly from Autodesk. Fully closing the direct-view path needs per-org APS credentials, not just per-org buckets. Pre-existing objects in the legacy kavai_cad_poc bucket are no longer referenced; those URNs now 403 on translate/status (re-upload places them in the org bucket). Unauthenticated by design: /api/health, /api/auth/sso, the chat proxies (/api/ag-ui-chat, /api/mcp-chat, /api/systems — identity travels inside the message), GET /api/coco-auto-mapper (usage text), GET /api/aps/demo-file (public sample), /api/test-image-visualization, and POST /api/gallery/assets/{assetId}/viewer-events (log-only stub).
2026-08-10 — the spec’s auth claims measured against the handlers
The worklist above asked whether an operation had a session check. It did not ask whether the operation’s declared scheme was true, and 21 were not — 12 declared SupabaseAuth and answered 401 to a valid JWT, 15 of the 21 being writes invisible to a runtime probe. All corrected; KNOWN_SECURITY_OVERSTATEMENTS in the drift script is empty and is a ratchet (an entry that stops applying fails the check).
Six defects found in the same pass, four of them holes the worklist’s own criteria would have missed: GET /images/{id}/gas-readings ran no session check over a SECURITY DEFINER RPC (unauthenticated readings); GET /datasets/{slug}/groups read across tenants (service-role, no membership check); POST /storage/validate-azure accepted Azure credentials from anonymous callers; viewers could remove asset links the DB refused them; has-gas-readings compared against memberships[0] only, 403-ing a user’s second organization; and seven handlers answered 400/500 before authenticating.
Still open: PUT /datasets/{slug}/3d-config authenticates and authorizes nothing — Dataset3DService.updateDataset3DConfig builds its own service-role client, so any authenticated user can write any dataset’s 3D config. The fix is auth.supabase, but the datasets UPDATE policy is owner-or-org-admin and would exclude ordinary members; the policy may be what is wrong. Also open: ~30 routes that authenticate and then read through a service-role client with a handler-side check, never audited systematically. Service-role usage is 44 route files, down from 54; four are auth.admin.* and cannot move.
Maintenance rule (when routes change)
- Update
web/public/api-docs/swagger.yaml: path under the right tag, real security declaration,$refshared schemas, actual error statuses. “Real” is enforced — the drift check reads the handler and fails a declaration the code does not honour. - Update the inventory page
docs/handbooks/content/web/api-inventory.md. - Sanity-check at
/api-docsin a dev server. No codegen in either direction. /api/ag-ui-chatSSE streaming is deliberately NOT modeled in Swagger — its contract is CONTRACT-CDC-001 (docs/handbooks/content/web/agui-contract.md).