Skip to content

Backend/API Contract

The backend/API contract is the bridge between frontend design changes and runtime behavior. When a new surface appears in the frontend, the matching backend capability should be identified, confirmed, or added before the change is considered complete.

  • Confirm every new data view has a backend endpoint or a documented static source.
  • Confirm every new action has a stable request path, method, payload shape, and response shape.
  • Preserve existing endpoints, response fields, and error behavior unless a migration is explicitly planned.
  • Add route coverage to the visual baseline list when a new user-facing surface is introduced.
  • Keep desktop, laptop, browser, PWA, and mobile behavior aligned with the same contract.
  • Confirm every client handles the shared request gates — 429 (with Retry-After / X-RateLimit-*) and 413 — on any route, not just the one being added (v0.55.0, 2026-06-21).
  • SpectraCheck uploads and analysis results
  • Raw FID archive preservation
  • Regulatory records and evidence trails
  • Reaction optimization workspaces
  • Deployment health, readiness, and diagnostics

These apply to every route, so they belong in the contract even though they add no endpoint. Both are settings-gated and default-OFF, so local dev and the test suite are unaffected; production enables them through the deployed service’s environment.

An in-app token-bucket limiter keyed system-key/admin → unlimited | user:{id}:{route} | ip:{client_ip}:{route}. The per-user key is the per-tenant key today — the product is single-tenant-per-user and AccessContext carries no org id.

  • Enforcement rides the router gates: _baseline_access_gate on the main router (reusing the already-resolved principal — no duplicate token decode), plus a rate-limit-only gate on the SCIM and nmr2d routers so privileged provisioning and the heavy 2D-analysis routes are throttled too.
  • A throttled call returns 429 with Retry-After and X-RateLimit-*; those headers are added to CORS_EXPOSE_HEADERS, so a browser client can read them.
  • Unauthenticated auth endpoints carry tight limits (login 10/min, sign-up and reset 5/min); everything else uses a generous default.
  • A throttle emits a de-duplicated SecurityEvent(event_type="rate_limit").
  • Fail-open — a limiter error can never 500 a request. The in-process bucket map is bounded (idle-then-LRU eviction) so key rotation cannot exhaust memory; the RateLimitStore protocol is the Redis drop-in seam.
  • Settings: RATE_LIMIT_ENABLED (default false), rate_limit_default_per_minute, rate_limit_burst_multiplier, rate_limit_trust_forwarded_for.

Request-body size cap (v0.55.0, 2026-06-21)

Section titled “Request-body size cap (v0.55.0, 2026-06-21)”

Non-multipart bodies larger than MAX_REQUEST_BODY_BYTES are rejected with 413. Multipart uploads are exempt — they have their own raw-archive caps. Default 0 (disabled).

A WAF is not implemented in-app: it is delivered as a Cloudflare/Vercel edge runbook, and the rate limiter is the testable in-repo enforcement (v0.55.0, 2026-06-21).

Method + pathPurposeNotes
GET /.well-known/security.txtRFC 9116 coordinated-disclosure file, served text/plain to anonymous callers.Expires is computed at request time and clamped into the one-year window, so a served file is never stale. Optional Policy / Canonical / Encryption / Acknowledgments URLs are emitted only when configured, so the file never advertises a page that 404s. Added to PUBLIC_ROUTE_PATHS, so it sits behind the same default-deny gate and IP-keyed rate limiter as /health. (v0.56.0, 2026-06-25)

Settings: SECURITY_TXT_ENABLED (default ON — the route 404s when disabled), security_txt_contacts, security_txt_expires_days, security_txt_policy_url, security_txt_canonical_url, security_txt_encryption_url, security_txt_acknowledgments_url, security_txt_preferred_languages. Operator-supplied values are CR/LF-sanitized so an env value cannot inject a forged field line.

The NMR analysis backend exposes a growing surface of typed endpoints. Each one writes a structured audit event so reviews remain reconstructible. The full capability detail with validation numbers lives in NMR Interpretation; the inventory below is the contract list a frontend or integrating system codes against.

Method + pathPurposeAudit event
POST /spectrum/analyze/gsdOpt-in Global Spectral Deconvolution; returns peaks + classifications + per-peak QC.spectrum.analyze_gsd
POST /spectrum/analyze/multipletsGroup GSD peaks into multiplets, recover J couplings, expose a synthetic forward modeller.spectrum.analyze_multiplets
POST /spectrum/analyze/integrationQuantitative region integration — Sum / Edited Sum / Peaks.spectrum.analyze_integration
POST /spectrum/predict/shiftsPredict ¹H / ¹³C shifts (ppm) + per-atom uncertainty (NMRNet or HOSE-code fallback).spectrum.predict_shifts
POST /spectrum/retrieveFAISS HNSW similarity retrieval — top-k nearest reference spectra by L2 distance.spectrum.retrieve
POST /spectrum/reasonRetrieval-augmented reasoning — retrieve precedent, then propose verifier-arbitrated candidate structures (graceful degradation when the index or model backend is absent).spectrum.reason
POST /candidates/compare/jcouplingMultiplet J-coupling unified-confidence bridge; per-candidate agreement labels + contradiction flags.confidence.candidates.multiplet_jcoupling_bridge
GET /spectrum/solvents/knownCanonical solvent catalog for FE dropdown validation.

Admin / operations endpoints (GSD soak loop)

Section titled “Admin / operations endpoints (GSD soak loop)”

These endpoints support the per-tenant graduation rollout for the opt-in GSD backend. See Deployment & Hosting → GSD experimental backend rollout for the operational policy.

Method + pathPurpose
GET /spectrum/analyze/gsd/telemetry-summary?window_days=NAggregate rollup — invocations, error rate, median/p95 wall time, solvent auto-detect rate, plus flip_readiness_verdict + flip_readiness_reasons + flip_readiness_policy + graduated_user_count + newly_graduated_in_window. Accepts an admin-only ?actor_user_id=<id> for per-tenant scope.
POST /admin/users/{user_id}/gsd-graduationAdmin action — graduate or ungraduate a tenant out of experimental: true, with a required reason (1–500 chars).
GET /admin/users/{user_id}/gsd-graduation-historyFull graduation history for one tenant — every graduate / ungraduate decision with the admin’s documented reason, newest-first.

Two read-only, admin-gated endpoints surface the Prompt 18 ops layer (see AI Model Lifecycle → MLOps) to the dashboard. Contract change (v0.21.1) — regenerate schema.d.ts.

Method + pathPurpose
GET /admin/ops/deployment-gateThe release-control posture, computed live: fails_closed (invariant), the gate’s self_check result, the four-check policy (dominance / audit_chain / tests_green / data_leakage), the output-contract schema version, and the monitoring thresholds (PSI / override / confidence bands + latency SLOs).
GET /admin/ops/model-lineageThe model-lineage dashboard — per production model: version, training-snapshot hash, gold metric vector, promotion record, supersession, and drift status. Returns a typed empty dashboard until a registry is wired and a model is promoted.

Admin / operations endpoints (security detections & SIEM)

Section titled “Admin / operations endpoints (security detections & SIEM)”

Two admin-gated (require_admin, behind the default-deny gate) endpoints turn the immutable SecurityEvent stream and the tamper-evident audit chain into near-real-time detections. Contract change (v0.58.0, 2026-06-26) — regenerate schema.d.ts.

Method + pathPurpose
GET /admin/security/alertsRead-only detection scan — runs the rules over the recent event window and returns the alerts without shipping them.
POST /admin/security/detections/runScan and ship to the SIEM sink. This is the cron hook.

Four pure detection rules back both routes: impossible_travel (same actor, two login_success from different IPs inside a window — an IP-velocity heuristic; geo enrichment is a documented seam), privilege_escalation (an is_admin flip), cross_tenant_access (≥ threshold cross_tenant_denied inside a window — enumeration probing), and audit_chain_break (ledger verification fails).

Contract detail callers and operators need (v0.58.0, 2026-06-26):

  • New response models SecurityAlert, DetectionScanResult, and the DetectionId enum.
  • SecurityEventType gains cross_tenant_denied and privilege_escalation, so any consumer that switches exhaustively over the event vocabulary must handle them.
  • Emission is wired into api.py at the three password-login routes (login_success + client IP), the three owner-scoped deny branches (cross_tenant_denied; anonymous and system principals skipped), and the three login admin-email auto-grant sites (privilege_escalation). MFA/SSO login and the explicit admin-grant route are documented seams, not yet emitting.
  • Sinks: always a JsonStdoutSink (structured {"siem_alert": …} to stdout → the platform log drain → any SIEM), plus a WebhookSink when SECURITY_ALERT_WEBHOOK_URL is set (best-effort POST, never raises). Only error/critical alerts ship.
  • Settings: SECURITY_SIEM_ENABLED (default on — no behavior change when false), SECURITY_ALERT_WEBHOOK_URL, plus the detection window/limit and the impossible-travel / cross-tenant thresholds. Emission is best-effort: a telemetry failure can never break the instrumented request.

Shipping to a hosted SIEM and running a 24/7 on-call rotation are operational, outside the repo; the rules, the sink seam, and the scan endpoints are the in-repo contract.

Endpoints that incident response depends on

Section titled “Endpoints that incident response depends on”

The incident-response plan maps its containment levers onto endpoints and store functions that already exist, which makes them load-bearing contract rather than incidental (v0.59.0, 2026-06-26): revoke a session family, revoke_all_user_tokens, SCIM deprovision, POST /admin/users/{id}/demote, and API / audit-signing key rotation. Its forensic evidence sources are likewise existing surfaces — audit-chain verify and search, the SecurityEvent stream, debug bundles, soft-delete retention, and e-signature verify. Treat all of them as stable; a breaking change to any one degrades the documented response path.

Regentry exposes the deterministic impurity engines and the dossier workflow. The whole /regulatory/dossiers/{id}/… surface is owner-scoped per user (own-or-system/admin); a non-owner read or write returns a non-leaking 404 (see Regentry → Access control).

Method + pathPurposeAudit event
POST /regulatory/impurities/assessUnified impurity assessment — Q3A/B thresholds, Q3C solvents, Q3D elementals, M7 mutagenicity, FDA CPCA nitrosamine, and cumulative-risk in one call from a product context; per-impurity failures degrade to warnings, never a 500. Contract change (v0.23.1).regulatory.impurity.assess
POST / GET /regulatory/dossiers/{id}/elemental-impurity-assessmentICH Q3D elemental-impurity assessment on a dossier (route-dependent PDEs; reads the dossier route). Contract change (v0.23.4, migration 0014).regulatory_compliance.elemental_impurity_assessment.create
GET /regulatory/dossiers/{id}/nitrosamine-cumulative-riskFDA-Rev-2 cumulative-risk rollup over a dossier’s nitrosamine watches (sum(measured / AI) < 1). Contract change (v0.23.5).
GET /regulatory/dossiers/{id}/readiness-reportList a dossier’s readiness reports, newest-first, so the workspace can rehydrate the latest. Contract change (v0.24.5).
GET / POST /regulatory/dossiers/{id}/ai-decisions · …/{entry_hash}/review · …/ai-decisions/verifyEU GMP Annex 22 (draft) AI-decision hash chain — record, list, HITL-review, and verify a per-dossier tamper-evident decision log. Contract change (v0.24.7, migration 0016).regulatory.ai_decision.*

The existing dossier assessment endpoints (…/residual-solvent-assessment, …/impurity-risk-register, …/nitrosamine-watch) now compute via the engines (v0.23.2) and source the dossier’s product context — max_daily_dose_g / substance_type (v0.23.3, migration 0013) and route (v0.23.4) added to the dossier models. The legacy-override is backward-compatible (tenant rule-rows still win when present), so v0.23.2 needs no schema.d.ts regen; v0.23.3 / v0.23.4 add the dossier fields and do.

Phase C wires only the reaction surfaces that work with no heavy dependency installed. The generative heavy paths (AiZynth route proposal, RXN / transformers forward prediction, torch GNN training, SDL execution) are deliberately not exposed and stay unwired until the site extras and an off-request worker exist. Contract change (v0.63.0, 2026-07-23, migration 0031, three tables) — regenerate schema.d.ts; the release ships its own frontend handoff note (docs/fe_handoff_reaction_phase_c.md).

The per-project routes are owner-scoped and sit behind the reaction module gate. The two global routes are not project-scoped. Capability detail lives in Reaction Optimization → Phase C engines and the capability readout.

Method + pathPurpose
POST / GET /reaction-projects/{id}/yield-predictions · …/{run_id}Fit a lightweight surrogate on the project’s own completed experiments and score submitted candidate conditions. The backend and its capability decision are recorded verbatim; degraded conditions are disclosed in per-prediction warnings.
POST / GET /reaction-projects/{id}/route-scores · …/{score_id}Score a chemist-supplied route (native or AiZynth-shaped tree) with the frozen safety / green engines; a Mermaid render is persisted with the record. Needs no optional dependency.
POST / GET /reaction-projects/{id}/forward-checks · …/{check_id}Cross-check a supplied forward prediction against the frozen engines before anyone acts on it. Needs no optional dependency.
GET /reaction-capabilitiesGlobal, stateless honesty readout of the governed heavy-ML capability table — per capability: enabled, available, active, missing_modules, reason, provenance, engine. Not project-scoped.
GET /reaction-sdl/statusRead-only SDL site status (enabled, the capability row, execution_surface_wired: false, a plain-language detail). Not project-scoped.

Contract detail callers need:

  • There is no SDL execution surface. No arm, run-step, or abort route exists, and a registered-routes test pins that absence — treat any client that expects one as wrong, not as awaiting an endpoint.
  • Error mapping. A capability that is flag-off or missing its dependency maps to 503. Recognised engine and parse errors surface as a 400 rather than a generic server error.
  • The three record types each carry a disclaimer and (route scores and forward checks) human_review_required. These are part of the response contract; any surface that renders them must preserve them.
  • The surface is covered by API tests, including migration 0031 upgrade / downgrade idempotence and the registered-routes test that pins the absence of an SDL execution surface.

The closed-loop feedback surface lets a reviewer rate an AI prediction and, optionally, tag why it was wrong — the structured reason rolls override analytics up where the model is weakest. Full detail in AI Model Lifecycle → Closed-loop feedback.

Method + pathPurposeAudit
POST /ai/predictions/{id}/feedbackRecord reviewer feedback on an AI prediction — a thumbs verdict plus an optional structured reason_code (wrong_shift / wrong_multiplicity / wrong_structure / missed_impurity / wrong_integration / calibration_off / other). Optional, nullable, additive.prediction-audit fan-out

The reason_code field is a contract change (v0.19.1) — regenerate schema.d.ts after this release.

The API contract now includes where the API lives, because the callable origin changed (2026-07).

  • The backend runs on Google Cloud Run — service moltrace-backend, project moltrace-prod, region us-central1, scale-to-zero. Render is fully retired; any client, runbook, or webhook still pointing at a Render URL is wrong.
  • The frontend stays on Vercel (moltrace.co) and reaches the API through a same-origin /api/backend proxy. Browser clients target the proxy path, not the Cloud Run origin.
  • Cloud SQL for PostgreSQL 16 sits on a private IP with no public interface, reached over Direct VPC egress. There is no publicly routable database endpoint to configure.
  • Cloud Storage holds the immutable raw-FID vault (moltrace-raw-vault, versioned), model weights, and the DVC remote. Secret Manager holds every credential, Cloud KMS holds the field-encryption key, and images build through Cloud Build into Artifact Registry.
  • CI/CD deploys the backend keylessly via Workload Identity Federation — no stored service-account key — behind the existing fail-closed release gate, so an unverified build still cannot reach production.

Cloud Run’s filesystem is ephemeral, so the write-once ALCOA+ raw-FID vault gained a GCS storage backend (2026-07). It preserves the same guarantees the local backend gives:

  • Writes are create-only, enforced with an if_generation_match=0 precondition — an existing object can never be overwritten.
  • SHA-256 verification runs both on reuse of an existing object and immediately after a write.
  • Bucket retention plus versioning is the WORM mechanism.
  • Selected with RAW_VAULT_BACKEND=gcs and RAW_VAULT_BUCKET. The local filesystem backend remains the default, so nothing changes for self-hosted or local deployments.

Several releases in the v0.53.0–v0.61.0 range shipped CI, library, or documentation work only and add no endpoint, request shape, or response field. They are listed here so a contract reviewer does not go looking:

  • v0.53.0 (2026-06-20) — secure-SDLC CI gates (SAST / SCA / IaC, CRITICAL-blocking). CI/repo config only.
  • v0.54.0 (2026-06-21) — CycloneDX SBOM per build, SLSA build provenance signed keylessly via Sigstore, and a verify-at-deploy gate that blocks every deploy hook on a verification failure. CI only, no application code.
  • v0.57.0 (2026-06-26) — IaC posture scoring with a drift gate, plus every GitHub Action uses: pinned to a 40-char commit SHA and a least-privilege default permissions: { contents: read }. CI/IaC/docs only.
  • v0.59.0 (2026-06-26) — the incident-response notification-deadline engine (src/nmrcheck/ir_timeline.py) is library-only, deliberately not wired into api.py. It computes timing, not legal conclusions, and sends nothing.
  • v0.60.0 (2026-06-26) — the restore-integrity verifier (src/nmrcheck/dr_verify.py) is a library plus CLI, no api.py route: python -m nmrcheck.dr_verify --min-rows audit_events=1,users=1 exits 0 verified / 1 failed / 2 cannot-connect. Operators call it after a restore; it is not an endpoint.
  • v0.61.0 (2026-06-29) — the control→evidence register (compliance/controls.json + validate_controls.py) is a repo-root, fail-on-drift CLI check plus the compliance-map, Trust Center, and sub-processor content. No application or runtime code. SOC 2 and ISO/IEC 27001:2022 are audited certifications MolTrace does not hold — the register and its docs are framed “designed to support / pursuing”, never “compliant” or “certified”, and any surface that renders this material must preserve that framing.

The FE↔BE contract is openapi.jsonnpm run generate:openapimoltrace_frontend/src/lib/api/schema.d.ts. Regenerate the schema after any release that adds a new endpoint or changes an existing request/response shape (every release flagged “Contract change — frontend must regenerate schema.d.ts” in the upstream moltrace_backend/CHANGELOG.md).

In the v0.53.0–v0.61.0 range the regeneration triggers are v0.56.0 (the /.well-known/security.txt route joins the public allow-list) and v0.58.0 (the two /admin/security/… routes plus the SecurityAlert / DetectionScanResult / DetectionId models and the two new SecurityEventType members; that release ships its own frontend handoff note). The remaining releases in the range add no schema.