Skip to content

API Changelog

v1.48 — case_id enforcement on FHIR list + MCP tools (#1194 C7, 2026-05-26)

BREAKING — public FHIR list endpoint

  • GET /api/v1/patients/{patient_id}/fhircase_id query parameter is now REQUIRED. Requests without it return HTTP 400 with error code FHIR_CASE_ID_REQUIRED_001. Previously the endpoint returned cross-case FHIR resources for the patient when case_id was omitted, which silently leaked unrelated clinical data into external LLM consumers.
  • Migration: append ?case_id=<case_uuid> to every call. Combine with the existing resource_type and active_only filters as before.
  • Cross-case reads: no longer supported via this public endpoint. Admin tooling with a legitimate need should call the repository layer directly (see docs/runbook/).

BREAKING — MCP tools

The Curaway MCP server (app/mcp/server.py) now requires case_id on two tools that fed clinical data into LLM context windows. Calls without case_id return a structured TextContent error and execute no query.

  • Tool get_patient_clinical_summarycase_id added to the inputSchema required array. Handler returns: "Error executing get_patient_clinical_summary: case_id is required (#1194 C7). …" when missing.
  • Tool run_matchcase_id added to the inputSchema required array. Handler returns: "Error executing run_match: case_id is required (#1194 C7). …" when missing. Match results include the clinical summary in the explanation field, so unscoped runs were the same leak surface that PR #1198 closed for the inline matching pipeline.

Why

Three enforcement points complete the C7 ring around the C-track work on issue #1194 (LLM context scoping). Together with E4+E2 (run_match plumbing, #1207), C6 (case_id-None tripwire, #1202), and C5 (CI scanner for ungated list_by_patient, #1203), every patient-FHIR read path that can reach an LLM now either takes a case_id or is loud about not having one.

New error code

Code HTTP Status Description
FHIR_CASE_ID_REQUIRED_001 400 case_id query parameter is required on GET /api/v1/patients/{patient_id}/fhir

v1.47 — Facilitator Attribution Layer (2026-05-03)

Admin facilitator management endpoints

  • GET /api/v1/admin/facilitators — List all facilitators (paginated). Response includes id, email, clerk_user_id, is_active, is_deleted, created_at, updated_at. Requires facilitator_admin:manage permission.

  • POST /api/v1/admin/facilitators — Create a new facilitator record. Required fields: email. Optional fields: phone, notes, name. Returns 409 FACILITATOR_DUPLICATE_EMAIL if email already in use. Requires facilitator_admin:manage permission.

  • PATCH /api/v1/admin/facilitators/{facilitator_id} — Update facilitator. Modifiable fields: email, phone, notes, name, is_active. Returns 404 FACILITATOR_NOT_FOUND if facilitator_id does not exist. Returns 409 FACILITATOR_DUPLICATE_EMAIL if updating email to one in use. Requires facilitator_admin:manage permission.

  • DELETE /api/v1/admin/facilitators/{facilitator_id} — Soft-delete facilitator. Sets is_deleted=true. Cannot delete if there are attributed records (cases/patients) without ?force=true flag (returns 409 FACILITATOR_HAS_ATTRIBUTED_RECORDS). Hard-delete with ?force=true requires admin:force permission (super_admin only). Requires facilitator_admin:manage permission.

Facilitator-scoped case retrieval

  • GET /api/v1/facilitator/sourced-cases — Retrieve cases attributed to the authenticated facilitator. Feature-flag gated (facilitator_sourced_cases_enabled). Returns 403 FEATURE_DISABLED if flag is off for the tenant. Returns cases where referred_by_facilitator_id matches the facilitator's id. Supports pagination.

Patient registration extension

  • POST /api/v1/patients/register — Gained optional field referred_by_facilitator_id (UUID string, nullable). When provided, validates that the facilitator exists and is active; returns 422 FACILITATOR_NOT_FOUND or 422 FACILITATOR_INACTIVE on mismatch. This field is automatically propagated to any cases created for the patient. Backwards compatible — existing callers omitting the field continue to work unchanged.

Clerk webhook integration

  • POST /webhooks/clerk — Handler for organizationMembership.created event now attaches facilitators.clerk_user_id when a Clerk user is invited to the facilitators org (tenant-curaway-facilitators). Emits new EventType CLERK_ORG_MEMBERSHIP_CREATED_FAC_LINKED on success. Collisions (email in Clerk but facilitator row not found) emit Telegram alerts (CRITICAL).

Database schema

  • New table facilitators (Alembic migration dc57d34f821b): stores facilitator profiles with audit trail. Columns: id (UUID), tenant_id, email, phone, notes, name, clerk_user_id, is_active, is_deleted, created_at, updated_at. Soft-delete pattern with RLS.

  • New column Patient.referred_by_facilitator_id (UUID FK, nullable): references the facilitator who referred/enrolled the patient. Populated at registration time if referred_by_facilitator_id query param supplied. ON DELETE SET NULL when facilitator is hard-deleted (GDPR Article 17).

  • New column Case.referred_by_facilitator_id (UUID FK, nullable): denormalized from Patient.referred_by_facilitator_id for query efficiency. Propagated at case creation time from the patient's referrer. Also ON DELETE SET NULL on facilitator hard-delete.

New EventType

  • CLERK_ORG_MEMBERSHIP_CREATED_FAC_LINKED: Fired when Clerk webhook successfully links a user to the facilitators table. Payload includes clerk_user_id, facilitator_id, and tenant_id.

No breaking changes

  • Existing endpoints (facilitator/delegated-cases, consent/*, admin tenants/users) remain unchanged in shape.

v1.46 — Streaming + WS lifecycle parity for triage path (2026-05-01)

Streaming chat — triage path

  • WS channel case:{case_id} now streams token events for the triage_agent path, matching the existing behavior of the legacy llm_conversation chat path. Previously triage_agent.conversation was invoke-only, producing a 5-12s silent pause before the full response appeared at once. Patient-facing latency on Path A is now on par with Path B. Fix landed in PR-D (#558).

No new WS event types — token and stream_end already existed. Triage now emits the same shape from the same channel.

WS lifecycle — findings_incorporated from doc-batch path

  • WS event findings_incorporated is now published from the document-batch handler in app/services/document_processing.py, in addition to the existing publish from app/routers/chat.py:564. Both publish sites guarantee the event fires whenever an assistant message is committed to a case conversation.

Background: the patient app's useWebSocket hook flips isReviewingBatch=true on batch_complete (showing the "Reviewing findings" overlay) and =false on findings_incorporated. Without the doc-batch publish, the overlay stuck for 5 minutes after every document upload — only the chat router was emitting the companion event.

Frontend behavior unchanged. Fix landed in PR #559.

No HTTP endpoint changes

  • No new or modified routes under /api/v1/. Both changes are refactors (PR-D) and a missing event publish (#559).

v1.45 — Admin tenant settings expansion (2026-04-30)

Modified endpoint

  • PATCH /api/v1/admin/tenants/{tenant_id} now accepts four new optional fields that update the tenant's tenant_settings row in the same call:
  • data_residency_region — pattern ^[a-z][a-z0-9-]{0,49}$ (e.g. us-east-1, eu-west-1, ap-south-1).
  • default_locale — BCP-47-ish, pattern ^[a-z]{2,3}(-[A-Z]{2})?$ (e.g. en, en-US, ar-SA).
  • default_currency — ISO 4217 alpha-3, pattern ^[A-Z]{3}$.
  • video_provider — pattern ^[a-z][a-z0-9_]{0,30}$ (e.g. daily, twilio).

All four are individually optional; the call can mix tenant-level fields (name, slug, etc.) and settings fields freely. The audit event's payload.fields_updated lists tenant fields directly and prefixes settings fields with settings. (e.g. settings.data_residency_region) so the audit log distinguishes them.

Response shape now includes a settings block when a settings row exists (returns null for legacy tenants without one):

{
  "tenant_id": "tenant-curaway-ops",
  "name": "Curaway Operations",
  "slug": "curaway-ops",
  "country_code": "USA",
  "contact_email": "ops@curaway.ai",
  "is_active": true,
  "settings": {
    "data_residency_region": "us-east-1",
    "default_locale": "en",
    "default_currency": "USD",
    "video_provider": "daily"
  }
}

When only tenant fields are in the patch, settings is loaded but not modified — its current values still come back so callers see the full state. When any settings field is in the patch but the tenant has no settings row (legacy migration gap), the endpoint returns 422 TENANT_INVALID_INPUT rather than silently dropping the update.

No new error codes — reuses existing TENANT_INVALID_INPUT / TENANT_NOT_FOUND_001 / TENANT_ALREADY_EXISTS.

Audit payload shape change (downstream consumers — heads up)

The payload.fields_updated list on admin.tenant_updated events now prefixes settings-level fields with settings. (e.g. settings.data_residency_region). Tenant-level field names are unchanged. Any external consumer (Metabase dashboard, Langfuse export, alert grep) that filters on the literal field name will need to widen its filter to match the new prefix.

Locale regex now accepts script subtags

default_locale regex was tightened from ^[a-z]{2,3}(-[A-Z]{2})?$ to ^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$ so zh-Hans, zh-Hant-TW, sr-Latn-RS etc. validate. Required for the SE Asia expansion per CLAUDE.md multilingual commitment.

6 service tests + 2 HTTP tests cover: settings + tenant fields combined, pure settings update, no-settings-row → 422, the legacy tenant-only update path that must NOT raise, locale script subtag acceptance, HTTP round-trip with settings response, HTTP 422 on malformed locale. 127 admin tests pass.


v1.44 — Admin audit log endpoint (2026-04-30)

New endpoint

  • GET /api/v1/admin/audit-log — paginated, filterable read over the append-only events table. Backs the audit log viewer on the admin portal. The data is already being written by every admin mutation endpoint (slices 2a/2b + slice 3d + #490); this just exposes it.
  • Permission: audit:read (super_admin / platform_admin only — one tier above user:read because audit payloads can include cross-tenant patient_ids and case_ids).
  • Feature flag: admin_user_management_enabled (paired with the rest of the admin surface).
  • Filters (all optional, AND semantics):
    • tenant_id (kebab-case)
    • actor_id (Clerk user_id pattern)
    • event_type (dotted convention, e.g. admin.tenant_created)
    • since (ISO 8601, inclusive lower bound on created_at)
    • until (ISO 8601, exclusive upper bound)
  • Pagination: limit (default 50, max 200) + offset.
  • Response shape: { events: [Event], limit, offset, has_more } ordered by created_at DESC. has_more is a peek-ahead boolean rather than a precise total because events is high-volume and COUNT(*) would be slow even with indexes. Frontend pagination is offset-based forward iteration.
  • New error code: 422 AUDIT_INVALID_RANGE when since >= until (operator typo, not a 500).
  • Every Event row includes: id, event_type, tenant_id, actor_id, patient_id, payload (FlexibleJSON, schema depends on event_type), correlation_id, source_service, created_at (ISO 8601 UTC).

9 HTTP tests cover: ordering, AND filters, actor_id filter, time-range filter (inclusive since + exclusive until), since >= until 422, peek-ahead has_more flag, RBAC 403 without audit:read, feature-flag 503, and empty-filter happy path. 121 admin tests pass.


v1.43 — Admin Clerk identity proxy endpoint (2026-04-30)

New endpoint

  • GET /api/v1/admin/users/{user_id}/identity — proxies the Clerk Backend API to hydrate the admin Users list with name + email + last_sign_in_at + image_url. The Clerk frontend SDK can't fetch arbitrary users (only the signed-in one), hence the proxy.
  • Permission: user:read. Feature flag: admin_user_management_enabled.
  • Response shape: { user_id, found, identity: ClerkIdentityCard | null } pinned via Pydantic so Clerk schema drift can't cascade silently.
  • Caching: clerk_identity_cache_ttl_sec (default 90s, dedicated setting). Shorter than the email cache because the cache key is user_id and a Clerk-side rename is invisible until expiry.
  • Differentiated 4xx error semantics (defense against transient config issues looking like deletions):
  • 404 → cached None, returns {found: false, identity: null}.
  • 401 / 403ClerkConfigError → 500 SYS_CONFIG_MISSING envelope. Not cached.
  • 429 / 5xxClerkUnavailableError → 503 CLERK_UNAVAILABLE. Not cached.
  • Other 4xxClerkUnavailableError (schema/protocol issue, surface to caller for retry).
  • PII-safe error messages. Errors include the clerk-trace-id response header (for support correlation) but not the response body — Clerk error bodies sometimes echo the requested email/user fragment.

No new error codes — reuses SYS_CONFIG_MISSING and CLERK_UNAVAILABLE from the email lookup endpoint.


v1.42 — Admin roles catalog endpoint (2026-04-29)

New endpoint

  • GET /api/v1/admin/roles — returns the canonical platform role catalog (the 8 system roles from app/models/role.py::RoleCode). Used by the admin Users + Tenants pages to populate the role-picker dropdown without hardcoding KNOWN_ROLE_CODES on the frontend (which drifts the moment a new role is seeded, e.g. provider_staff).
  • Permission: user:read — paired with GET /api/v1/admin/users.
  • Feature flag: admin_user_management_enabled (shared with the Users surface; one toggle hides both).
  • Response shape: { roles: [{ id, code, name, description, tenant_type, is_system, permissions? }], total } ordered by code ASC. The permissions[] field is only included for callers carrying rbac:manage (super_admin / platform_admin). A plain user:read viewer gets the dropdown payload without the platform's permission taxonomy — the field is omitted, not nulled, so a typo on the consumer side fails loudly instead of treating "no permissions" as "empty list".
  • is_system is always true today (all rows are system roles); the field is exposed forward-compatibly for the eventual custom per-tenant roles feature (deferred per app/models/role.py docstring).
  • No pagination — catalog is 8 system roles. If custom per-tenant roles ever ship, add tenant_id filter + limit/offset then.
  • No new error codes. 403 AUTH_PERMISSION_DENIED (emitted by require_permission) and 503 FEATURE_DISABLED reuse the existing taxonomy.

5 HTTP tests cover the happy path, empty-catalog edge case, the RBAC gate (with explicit error_code assertion), the permissions-strip contract for non-rbac:manage callers, and the feature-flag 503. 102 admin tests still pass.


v1.41 — Admin cleanup queue (2026-04-29)

Backend hygiene fixes

  • X-Result-Truncated response header on GET /api/v1/admin/users when the unfiltered total exceeds MAX_SEARCH_RESULTS (10,000). Body also carries truncated: true so SPA frontends can warn the user to narrow filters. Closes #477.
  • POST /api/v1/admin/tenants body field rename — Python field is now tenant_id (was id). Wire-level field stays id via Pydantic alias; no breaking change for clients. Internal consistency for callers that destructure body.id (which collided with Python's id builtin).
  • extra='forbid' on every admin Pydantic body model. Stray fields in request bodies now return 422 — defends against silent field-drop on the idempotent comparator (e.g. note on grant).
  • TenantOrgMappingRepository._flush() now wraps SQL errors as typed domain errors (DuplicateRecordError, StoreUnavailableError) per ADR-0016 — same shape as TenantRepository. Closes #481.
  • Shared AdminGuardError base classLastAdminGuardError and ActiveCasesGuardError now share app.services._admin_errors. Future guards (e.g. "deactivate provider with active matches") slot in without duplication.
  • pg_advisory_xact_lock on deactivate_tenant — same pattern as revoke_role's last-admin guard. Prevents duplicate audit events from concurrent deactivates.
  • UserRoleAdminRepository.list_user_role_pairs_for_tenant — single JOIN replaces N+1 in AdminTenantService.list_members (was: search_user_ids
  • per-user list_user_role_pairs).
  • TenantRepository.update_fieldsupdate_non_none_fields — explicit name documents that None means "skip", not "set NULL".

No new endpoints. No new error codes (response shape unchanged on errors). 95 admin tests pass (was 90).


v1.40 — Admin Tenants + org-mapping CRUD (2026-04-29)

Backend

New endpoints

Tenant CRUD (/api/v1/admin/tenants/*):

Method Path Permission Notes
GET /api/v1/admin/tenants tenant:read Search by name/slug substring (LIKE-escaped). Filter by country_code, active_only. Paginated. Includes member_count and active_case_count per row.
POST /api/v1/admin/tenants tenant:manage Create non-provider tenant (idempotent on id). Body: {id, name, slug, contact_email, country_code, type?}. type ∈ {coordinator, mso, admin, facilitator}. Provider tenants must use POST /api/v1/admin/providers/{id}/tenant (existing). Returns 201.
GET /api/v1/admin/tenants/{tenant_id} tenant:read Detail: name, settings, member_count, active_case_count, org_mappings.
PATCH /api/v1/admin/tenants/{tenant_id} tenant:manage Update fields. Setting is_active=false is rejected — use DELETE for the active-cases guard.
DELETE /api/v1/admin/tenants/{tenant_id} tenant:manage Soft-deactivate. force=true bypasses active-cases guard, requires admin:force.
GET /api/v1/admin/tenants/{tenant_id}/members tenant:read List user_roles rows for the tenant + user_ids (frontend hydrates Clerk identities).

Org-mapping CRUD (/api/v1/admin/org-mappings/*):

Method Path Permission Notes
GET /api/v1/admin/org-mappings tenant:read List Clerk org → tenant mappings. Filter by tenant_id, environment, org_role.
POST /api/v1/admin/org-mappings org_mapping:manage Create mapping. Body: {clerk_org_id, tenant_id, org_role, environment}. Returns 201.
DELETE /api/v1/admin/org-mappings/{id} org_mapping:manage Hard delete (mapping is config, not patient data).

New event types (audit log)

  • admin.tenant_created
  • admin.tenant_updated
  • admin.tenant_deactivated
  • admin.org_mapping_created
  • admin.org_mapping_deleted

Plus reuses admin.force_used from slice 2a when force-deactivating a tenant with active cases.

New error codes (see error-codes.md)

  • TENANT_NOT_FOUND_001 (404)
  • TENANT_ALREADY_EXISTS (409)
  • TENANT_INVALID_INPUT (422)
  • TENANT_ACTIVE_CASES_GUARD (409)
  • ORG_MAPPING_NOT_FOUND (404)
  • ORG_MAPPING_DUPLICATE (409)

Pydantic regex validation

  • tenant_id body/path: ^[a-z][a-z0-9-]{1,35}$
  • slug: ^[a-z][a-z0-9-]{1,99}$
  • country_code: ^[A-Z]{3}$ (ISO 3166-1 alpha-3)
  • clerk_org_id: ^org_[A-Za-z0-9]{16,40}$
  • environment: ^(production|staging|test)$
  • tenant_type: ^(coordinator|mso|admin|facilitator)$

Closes spec gap #476AdminTenantService.assert_tenant_exists is the shared validation hook for callers that need "tenant exists" before writing rows referencing it (e.g. admin_user_service.grant_role).

Slice 2a followup also landed (carryover from PR #479 review):

  • TOCTOU advisory lock now uses two-key pg_advisory_xact_lock(role, tenant) for 64-bit collision space.
  • Path user_id validation moved to FastAPI Path(..., pattern=...) — 422 fires from the standard pipeline before feature-flag check.
  • revoke_role with bogus role_id now raises 422 RBAC_INVALID_ROLE (was incorrectly 500); true orphan FK on an active row still raises 500 RBAC_DATA_INTEGRITY.

See PR #(slice 2b PR).


v1.39 — Admin Users endpoints (2026-04-29)

Backend

New endpoints (/api/v1/admin/users/*, gated by user:read / user:manage)

Method Path Permission Notes
GET /api/v1/admin/users user:read Search users across tenants. Filters: q (Clerk user_id substring, ILIKE-escaped), tenant_id, role_code, active_only. Paginated with limit (≤100) + offset.
POST /api/v1/admin/users/by-email/lookup user:read Body {email}. Resolves Clerk email → user_id. Cached for 5 min (bounded LRU). 503 CLERK_UNAVAILABLE on outage; 500 SYS_CONFIG_MISSING if CLERK_SECRET_KEY unset.
GET /api/v1/admin/users/{user_id} user:read User detail: roles across tenants + aggregate counts. Frontend hydrates Clerk identity (name, email) client-side.
GET /api/v1/admin/users/{user_id}/roles user:read All user_roles rows for the user, optional active_only.
POST /api/v1/admin/users/{user_id}/roles user:manage Body {role_code, tenant_id, note?}. Idempotent grant — insert / reactivate / no-op. note recorded on audit Event.
DELETE /api/v1/admin/users/{user_id}/roles/{role_id} user:manage Soft revoke. ?force=true bypasses last-admin guard, additionally requires admin:force (super_admin only). PG advisory lock prevents TOCTOU race on the guard.

New event types (audit log)

  • admin.user_role_granted — every successful POST to grant a role
  • admin.user_role_revoked — every soft revoke
  • admin.self_mutation — actor == target on grant/revoke
  • admin.force_usedforce=true used to bypass last-admin guard

New error codes (see error-codes.md)

  • RBAC_INVALID_ROLE (422) — body role_code unknown
  • RBAC_INVALID_FILTER (400) — query-string or path filter malformed
  • RBAC_LAST_ADMIN_GUARD (409) — revoke would leave 0 active holders
  • RBAC_FORCE_DENIED (403) — force=true without admin:force permission
  • RBAC_DATA_INTEGRITY (500) — orphan role_id reference
  • AUTH_NO_ACTOR (401) — caller identity missing on authenticated route
  • FEATURE_DISABLED (503) — endpoint disabled by feature flag
  • AUDIT_WRITE_FAILED (500) — DB commit failed; rolled back, retryable
  • CLERK_UNAVAILABLE (503) — Clerk API failure (transient)
  • SYS_CONFIG_MISSING (500) — required env var unset (e.g. CLERK_SECRET_KEY)

New feature flags (see feature-flags.md)

  • admin_user_management_enabled (default false) — gates /admin/users/*
  • admin_tenant_management_enabled (default false) — gates /admin/tenants/*

Both default OFF per spec rollout: identity-override SD via Flagsmith, then 100%.

Pydantic regex validation added to admin Users endpoints: - Path user_id must match Clerk format ^user_[A-Za-z0-9]{16,40}$ - Body role_code must match ^[a-z][a-z0-9_]{1,63}$ - Body/query tenant_id must match ^[a-z][a-z0-9-]{1,63}$

Feature-flags SDK extension

  • is_feature_enabled(flag, tenant_id, *, identity=None)identity (kwarg) overrides tenant_id as the Flagsmith identifier so per-user identity overrides work for admin/SD-only rollouts.
  • get_feature_value mirrors the same identity precedence.

See PR #475 (slice 2a base) and #(slice 2a followup).


v1.38 — Admin portal + Clerk webhook (2026-04-28)

Backend

Performance

  • ICD-10 mapping (medical_extractor) now hits a Redis cache (30-day TTL) keyed on the normalized diagnosis+procedure pair. Top-50 diagnoses cover ~80% of cases; expect ~75% hit rate after warm-up. Toggleable via icd_cache_enabled flag (default on). See PR #460 / issue #349.

Refactor

  • Sweep endpoint (/api/v1/internal/documents/sweep) and the QStash-driven OCR pipeline (run_ocr_pipeline) now go through DocumentRepository / new DocumentAdminRepository per ADR-0016. New methods: get_for_internal_ocr, get_for_internal_ocr_no_active_filter, atomic_claim_for_processing, find_stuck_global. No behavior change. See PR #461 / issue #273.

New endpoints

Method Path Description
GET /api/v1/admin/flags List Flagsmith feature definitions + per-environment states
GET /api/v1/admin/flags/{feature_state_id} Single flag detail (env value + identity overrides)
PATCH /api/v1/admin/flags/{feature_state_id} Update a flag's value (audited via admin.flag_changed)
POST /api/v1/admin/flags/{feature_name}/identities/{identifier} Set per-identity override (audited via admin.flag_identity_override)
GET /api/v1/admin/matching/config Read active matching weights + engine defaults
PATCH /api/v1/admin/matching/config Update matching weights (audited via admin.matching_config_changed)
POST /api/v1/webhooks/clerk Clerk → Svix webhook receiver. Today handles organizationMembership.deleted; other event types ack 200 with status=ignored

All admin endpoints require the feature_flag:manage permission (granted to platform_admin). The Clerk webhook is gated by Svix signature verification only — server-to-server, not RBAC.

New event types (audit log)

  • admin.flag_changed — every PATCH to a Flagsmith flag's value
  • admin.flag_identity_override — per-identity flag override creation
  • admin.matching_config_changed — matching weights update
  • clerk.org_membership_deleted — successful org-deletion webhook
  • clerk.webhook_rejected — webhook with invalid Svix signature

New env vars (Railway)

  • FLAGSMITH_ADMIN_TOKEN — Svix admin scope token (required for /admin/flags/* and /admin/matching/config)
  • FLAGSMITH_PROJECT_ID — numeric project id from Flagsmith dashboard
  • FLAGSMITH_ENVIRONMENT_KEY — server-side env key (ser.…)
  • FLAGSMITH_ADMIN_API_URL — defaults to https://api.flagsmith.com/api/v1/
  • CLERK_WEBHOOK_SECRET — Svix signing secret (whsec_…) from Clerk dashboard
  • DEFAULT_TENANT_ID — defaults to tenant-apollo-001; lets us flip the platform default tenant without code edits

New required Flagsmith flag

  • matching_weights_v1 — STRING/MULTIVARIATE, JSON-encoded dict with the 5 keys clinical_fit, outcomes, cost, travel, preferences. Sum must be ≈ 1.0 (±0.01). The matching engine reads this at runtime; admin endpoint validates the same contract before write.

Behavior change

  • WeightedScoringV1.WEIGHTS now JSON-parses the Flagsmith flag value before applying. Previously the dict-type check on a string never passed and every override was silently ignored. Misconfigured flags now log at WARNING and fall back to engine defaults.

Frontend

Admin portal (apps/admin-app/) gets two functional tabs: - /flags — searchable table with toggle + value-edit, optimistic updates - /matching — five sliders for matching weights, live total indicator, save gated on sum=100%

Migration

No DB migration. Driven entirely by env vars + Flagsmith flag registration.

Refs

  • PRs: #451, #131 (frontend), #452, #132 (frontend), #455, #456, #457
  • Deploy checklist: #454

v1.37 — Facilitators + Coordinators Phase 3 (2026-04-19)

Backend

New Models

  • Coordinator — Curaway staff coordinator with tier, language skills, active case count
  • CoordinatorAssignment — links coordinator to case with assignment timestamp
  • TransportVendor — transport vendor directory (airport, hotel, hospital)
  • TransportBooking — coordinator transport booking per case
  • TimelineMilestone — case lifecycle milestones (pre-op, travel, admitted, discharge, follow-up)
  • CoordinatorNote — free-text notes on cases by coordinators
  • Escalation — escalation events with type (coordinator_manual, system_overdue), severity, resolution
  • Rating — CSAT ratings for coordinators and facilitators post-case
  • FacilitatorConsent — patient-granted access delegation to facilitators

New Endpoints

Method Path Description
POST /api/v1/coordinator/assign Auto-assign coordinator to case at payment_locked
GET /api/v1/coordinator/queue Coordinator case queue with filtering and pagination
GET /api/v1/coordinator/cases/{case_id} Full case detail view for coordinator
GET /api/v1/coordinator/vendors Transport vendor directory
POST /api/v1/coordinator/bookings Create transport booking
PATCH /api/v1/coordinator/bookings/{booking_id} Update transport booking
GET /api/v1/coordinator/bookings List coordinator's bookings
GET /api/v1/cases/{case_id}/timeline Case timeline milestones and events
PATCH /api/v1/cases/{case_id}/timeline/milestone/{name} Update milestone status
POST /api/v1/cases/{case_id}/timeline/note Add coordinator note to case
POST /api/v1/escalations Raise an escalation
GET /api/v1/coordinator/escalations Coordinator's open escalations
PATCH /api/v1/escalations/{escalation_id} Resolve or update an escalation
POST /api/v1/ratings/submit Submit CSAT rating for coordinator or facilitator
GET /api/v1/coordinator/{coordinator_id}/performance Coordinator performance metrics
POST /api/v1/consent/facilitator/grant Patient grants facilitator access to case
POST /api/v1/consent/facilitator/revoke Patient revokes facilitator access
GET /api/v1/consent/facilitator/list List active facilitator grants for a case
GET /api/v1/facilitator/delegated-cases Facilitator's accessible delegated cases

Feature Flags

Flag Default Purpose
facilitator_consent_enabled false Enable facilitator consent grant/revoke flow
coordinator_assignment_enabled false Enable auto-assignment at payment_locked
coordinator_queue_enabled false Enable coordinator queue dashboard
coordinator_services_transport_v1 false Enable transport vendor directory + booking
coordinator_timeline_enabled false Enable case timeline milestones and notes
coordinator_escalation_enabled false Enable escalation triggers
coordinator_csat_enabled false Enable CSAT/ratings collection

v1.36 — Risk Assessment + MSO Second Opinion Phase 2 (2026-04-19)

Backend

New Models

  • RiskReview — risk assessment record with score, blocking factors, reviewer decision
  • RiskFactor — individual risk factor with source provenance and is_blocking flag
  • MSOMatcher — MSO specialist matching model
  • MSOConsultation — MSO consultation record (document_review or video)

New Endpoints

Method Path Description
GET /api/v1/risk/queue Risk review queue for reviewers (requires risk_reviewer permission)
GET /api/v1/risk/{case_id}/assessment Full risk assessment for a case
POST /api/v1/risk/{case_id}/decision Reviewer approve/reject/request-info decision
GET /api/v1/risk/{case_id}/history Full decision history for a case
POST /api/v1/cases/{case_id}/mso/request Patient requests MSO second opinion
GET /api/v1/cases/{case_id}/mso/status Current MSO consultation status
GET /api/v1/mso/consultations MSO doctor's consultation inbox
GET /api/v1/mso/consultations/{consultation_id} MSO consultation detail
POST /api/v1/mso/consultations/{consultation_id}/document MSO doctor submits document review
POST /api/v1/mso/consultations/{consultation_id}/records-request MSO doctor requests additional records

Case State Transitions Added

consent_givenrisk_review_pendingrisk_clearedproviders_notifiedmso_offeredmso_completepayment_locked

Feature Flags

Flag Default Purpose
risk_assessment_enabled true Gate the entire risk workflow
risk_review_human_required true All cases require human review
risk_blocking_override_allowed false Allow reviewers to override blocking factors
mso_consultation_enabled false Enable MSO matching + consultation flow
mso_video_embedded false Use Daily.co embedded video rooms

v1.35 — Provider Flow Phase 1 (2026-04-19)

Backend

New Models

  • ProviderQuote — cost breakdown (line items in USD cents), status lifecycle (draftsubmittedselected / rejected)
  • redacted_snapshot column on case_shares — stores provider-visible redacted case payload

New Endpoints

Method Path Description
GET /api/v1/provider/cases Provider inbox — cases forwarded to this provider's tenant
POST /api/v1/provider/cases Accept or action a forwarded case (provider-side)
POST /api/v1/provider/cases/{id}/quote Submit a quote for a forwarded case
POST /api/v1/provider/cases/{id}/decline Decline a forwarded case
GET /api/v1/cases/{id}/quotes Patient-facing quote comparison (gated by provider_portal_v1)
POST /api/v1/admin/providers/{id}/tenant Assign a provider to a tenant (admin only)
POST /api/v1/admin/providers/{id}/invite-staff Invite provider staff member via Clerk multi-org mock

New Services

  • Case forwarding service — applies provider_snapshot redaction, creates case_share, triggers provider notification
  • Provider notification service — events: case_forwarded, quote_received, provider_selected
  • Provider onboarding service — Clerk multi-org mock (no live webhook yet; deferred to Phase 2)

Alembic Migration

  • provider_quotes table: quote line items (JSONB), status, submitted_at, currency (ISO 4217), total_cents
  • redacted_snapshot JSONB column on case_shares

Feature Flags

Flag Default Purpose
provider_forwarding_v1 false Enable case forwarding to provider tenants
provider_clerk_integration false Enable Clerk multi-org invite flow (mock in Phase 1)
provider_portal_v1 false Enable provider portal API + patient quote comparison

Frontend

  • Provider portal (apps/provider-app): case inbox, case detail view, quote builder UI
  • API wiring with demo mode fallback (used when provider_portal_v1 flag is off)
  • Design system app (apps/design-system) — shared component library bootstrapped

v1.34 — Multi-Tenancy Phase 0 (2026-04-18)

Backend

RBAC

  • 7 roles (patient, facilitator, coordinator, mso_doctor, provider_admin, platform_admin, super_admin), 22 permissions
  • roles and user_roles tables with UUID PKs; roles are a global catalog (not tenant-scoped)
  • RBACServicehas_permission(), assign_role(), revoke_role(), check_or_raise()
  • RBACMiddleware — resolves user roles from Clerk JWT on every request, sets request.state.permissions
  • @require_permission(perm) route decorator for fine-grained endpoint access control

Case Shares

  • case_shares table with dual-scope RLS (source_tenant_id OR target_tenant_id = app.tenant_id)
  • SharingMode: copy (provider snapshots) or reference (coordinator / MSO / facilitator)
  • RedactionPolicy: provider_snapshot, mso_clinical, coordinator_full, facilitator_consent
  • CaseShareService — create/revoke shares, consent grant/revoke for facilitators

Redaction Engine

  • 4 policies defined in config/redaction_policies.yaml
  • RedactionEngine singleton — applies field-level masking (pseudonymize, age-only, budget-range, remove)
  • RedactedCaseData typed dataclass prevents unintentional PII leaks

Case Lifecycle State Machine

  • 27 states via python-statemachine in app/state_machine/case_machine.py
  • CaseLifecycleService — sole writer of case.status; every transition is audit logged
  • Backward-compatible: runs in parallel behind case_state_machine_v2 flag

Modified Endpoints

Method Path Change
GET /api/v1/cases/{id} Response now includes layer_state when triage_agent_v3 flag is on
GET /api/v1/cases/{id}/scores New — returns PFS / HSS / FMS scoring breakdown

Alembic Migration

  • roles table + seed (7 system roles from config/rbac_defaults.yaml)
  • user_roles table + RLS
  • case_shares table + dual-scope RLS + composite unique indexes

Feature Flags

Flag Default Purpose
rbac_enforcement false Enable permission checks on routes
case_state_machine_v2 false Use python-statemachine instead of status list
redaction_engine false Enable redaction at case share creation
wave2_layer_ui false Gate layer_state field in case responses

v1.33 — SSE Clerk Auth + Conversation Flow Gates v2 + Risk Assessor

Date: 2026-04-07

SSE Endpoints Now Require Clerk JWT

All three SSE endpoints now require a Clerk JWT supplied via ?token=<jwt> query parameter (since EventSource cannot send Authorization headers). Verified by verify_sse_token in app/services/auth.py.

Method Path Auth
GET /api/v1/patients/{id}/documents/stream ?token=<clerk-jwt> (required)
GET /api/v1/cases/{id}/chat/stream ?token=<clerk-jwt> (required)
GET /api/v1/cases/{id}/messages/stream ?token=<clerk-jwt> (required)

Tenant precedence (hotfix): When both a ?tenant_id= query param and a Clerk org_id claim are present, the caller-supplied query value wins. Patient rows are partitioned under tenant slugs (e.g. tenant-apollo-001), not Clerk org IDs, so the explicit fallback must override.

Forwarding Step: on_site_tests_notice Rich Card

The forwarding step now emits an on_site_tests_notice rich card aggregating Neo4j REQUIRES_TEST requirements + per-provider REQUIRES_FOR_PROCEDURE overrides — tells the patient which tests must be repeated on-site at the receiving hospital.

Conversation Flow Gates v2 (gates_v2 flag, default on)

_intake_complete_v2 replaces the legacy six-AND gate with a five-condition check (procedure + evidence + age/country + meds-or-skip + allergies-or-skip). completeness_for_matching lowered from 0.5 to 0.4. Patient explicit-advance phrases ("find providers now", "i'm ready", "proceed") promote skip flags and fall through to matching on the same turn.

New events: gates_v2.intake_complete, gates_v2.explicit_advance, gates_v2.matching_advanced.

Pre-Operative Risk Assessor

ehr_builder_agent now runs app/services/risk_assessor.py (rule-based, no LLM) on every EHR rebuild. Populates ehr_snapshot.risk_factors with age / comorbidity / medication / lab risks, each carrying source provenance and an is_blocking flag. Blocking risks halt forwarding.

Records-First Real Tests

Records-first phase now pulls case_service.get_procedure_requirements() (Neo4j-backed via procedure_reqs_from_graph) into the LLM prompt instead of hallucinating tests. Formatted with validity windows, source acceptance, and on-site flags.


v1.32 — SSE Upload Progress + Inline OCR

Date: 2026-04-05

SSE Document Progress Streaming

Real-time upload processing progress delivered via Server-Sent Events.

Method Path Description
GET /api/v1/patients/{id}/documents/stream SSE stream for document processing progress (dual-source: Redis 300ms + DB 5s polling)

Progress Pipeline (6 steps)

upload_received -> ocr_started -> ocr_complete -> analysis_started -> analysis_complete -> matching_complete

Each step emits a ProgressEvent with step, label, status, timestamp, detail, and document_id.

New SSE Event Types

Event Channel Payload
progress doc_progress:{patient_id} {step, label, status, timestamp, detail?, document_id}
heartbeat /documents/stream {timestamp}

Inline OCR Fast Path

  • PyMuPDF runs inline during confirm_upload for native PDFs (>100 chars extracted)
  • Saves 200-500ms QStash round-trip; falls back to QStash for scanned/image PDFs
  • Shared pipeline in app/services/document_processing.py with emit_progress() helper

New Schemas

  • ProgressStep enum (6 steps) and ProgressEvent schema in app/schemas/document.py

Feature Flag

  • enable_inline_ocr — controls whether inline fast path fires emit_progress during confirm_upload

v1.31 — Latency Optimization + Test Coverage Sprint

Date: 2026-04-04

Performance (13.2s -> 7.9s average, -41%)

  • Timing middleware: profiles every request segment (auth, state, LLM, DB, cache). Adds X-Request-Timing-Ms header.
  • Chat pipeline caching: patient state (60s TTL), conversation history (120s TTL) via Redis cache-aside.
  • Parallel LLM calls: input classifier concurrent with context fetch via asyncio.gather().
  • Deferred extraction: chat extractor + EHR updates moved to background tasks after response streams.
  • LLM response streaming: llm.astream() pushes tokens via Redis SSE to GET /cases/{id}/chat/stream.
  • Connection pooling: LLM clients now singletons. SQLAlchemy pool_recycle=1800, pool_timeout=30.
  • Prompt compression: system prompts reduced 36-73% (v2_compressed).
  • Inline OCR fast path: PyMuPDF inline during confirm_upload, skips QStash for structured PDFs.

New Endpoints

Method Path Description
GET /api/v1/cases/{id}/chat/stream SSE stream for LLM response tokens (0.1s Redis poll)

Neo4j Procedure Requirements Wiring

  • graph_service.get_procedure_requirements_for_agents() transforms Neo4j REQUIRES_TEST data
  • CPT-to-slug mapping (14 procedures) fixes code mismatch
  • case_service.get_procedure_requirements() routes through Neo4j with PostgreSQL fallback

Test Coverage

  • 760 backend tests (0 failures, up from ~390 with 31 failures), 32 frontend tests
  • 12 new test files (7,772 lines)

New Feature Flags (9 flags)

enable_timing_middleware, enable_chat_cache, enable_parallel_pipeline, enable_deferred_extraction, enable_response_streaming, enable_inline_ocr, enable_doc_pipeline_timing, prompt_version, procedure_reqs_from_graph


v1.30 — Procedure Requirements + System Landscape

Date: 2026-04-04

Provider Override Management (2 endpoints)

Method Path Description
POST /api/v1/providers/{slug}/procedures/{code}/requirements/{test_code} Create/update provider-specific test requirement override
DELETE /api/v1/providers/{slug}/procedures/{code}/requirements/{test_code} Delete provider-specific test requirement override

System Landscape (renamed from /health)

Method Path Description
GET /landscape System landscape HTML dashboard (renamed from /health)
GET /landscape.json System landscape JSON API
GET /health 301 redirect to /landscape (backward compat)

Internal Services (no new endpoints)

  • Chat Extractor: Dedicated Haiku call on every chat turn extracts medications, allergies, demographics from conversation
  • LLM Requirement Matcher: Haiku call matches document observations against procedure requirements after clinical extraction
  • Agent Tools Card: 18 agent capabilities displayed on landscape page

New Seed Data

  • seed_procedure_tests.py: REQUIRES_TEST for 15 procedures (137 test mappings)
  • Provider overrides: 7 entries for Apollo Chennai, Bumrungrad, Acibadem

v1.28 — Traceability, Context Engine & Provider Storefront

Date: 2026-04-01

Traceability & Feedback (6 endpoints)

Method Path Description
GET /api/v1/cases/{id}/decisions Full decision trace for a case
POST /api/v1/cases/{id}/provider-feedback Provider submits EHR corrections
POST /api/v1/cases/{id}/satisfaction Patient NPS survey
GET /api/v1/internal/eval/summary Eval metrics dashboard
GET /api/v1/internal/feedback/pending Unapplied corrections
POST /api/v1/internal/feedback/{id}/applied Mark correction applied

Public Storefront (9 endpoints)

Method Path Description
GET /api/v1/public/providers Provider listing (filterable, paginated)
GET /api/v1/public/providers/{slug} Provider detail with facilities, doctors, travel
GET /api/v1/public/doctors Doctor listing (filterable)
GET /api/v1/public/doctors/{slug} Doctor detail
GET /api/v1/public/treatments Treatment categories with provider counts
GET /api/v1/public/treatments/{slug} Treatment detail with providers
GET /api/v1/public/destinations Country listing with provider counts
GET /api/v1/public/destinations/{slug} Country detail with providers
GET /api/v1/public/search Unified search (providers, doctors, treatments, destinations)

Internal Eval (5 endpoints)

Method Path Description
POST /api/v1/internal/eval/extraction-accuracy Nightly extraction eval
POST /api/v1/internal/eval/match-quality Weekly match quality eval
POST /api/v1/internal/eval/question-relevance Weekly question relevance eval
POST /api/v1/internal/eval/feedback-patterns Daily correction pattern detection
POST /api/v1/internal/eval/weight-optimizer Monthly weight optimization

New Tables

  • feedback_records — Decision-outcome links with correction lifecycle
  • provider_facilities — Provider facility listings (name, description, icon)
  • treatment_categories — Treatment/procedure categories for storefront

New Error Codes

  • 12 PUBLIC_* codes (PUBLIC_PROVIDER_NOT_FOUND_001 through PUBLIC_SLUG_CONFLICT_012)

New Feature Flags

  • PUBLIC_API_ENABLED, STOREFRONT_PAGES_ENABLED, PUBLIC_SEARCH_ENABLED, STOREFRONT_CACHE_ENABLED, PROVIDER_COMPLETENESS_DISPLAY

v1.27B — Bug Fixes (Session 27B)

Date: 2026-03-31

  • First-message attachment processing alongside procedure identification
  • EHR deduplication (conditions by ICD-10, observations by parameter name)
  • New Case button no longer auto-switches to active case
  • Auto case title from procedure_name in ChatResponse

v1.26 — Doctors Schema (Session 26)

Date: 2026-03-31

New Endpoints

Method Path Description
GET /api/v1/doctors/ List doctors (paginated, filterable by provider_id, specialty)
GET /api/v1/doctors/search Search doctors by specialty, procedure_code, language, gender, min_volume
GET /api/v1/doctors/{doctor_id} Get doctor by UUID
GET /api/v1/doctors/slug/{slug} Get doctor by URL slug
POST /api/v1/doctors/ Create doctor profile (admin)
PATCH /api/v1/doctors/{doctor_id} Update doctor profile (admin), recalculates data completeness
GET /api/v1/doctors/{doctor_id}/procedures List procedures a doctor performs
POST /api/v1/doctors/{doctor_id}/procedures Add procedure to doctor (admin)
GET /api/v1/providers/{provider_id}/doctors List doctors at a specific provider

Modified Responses

  • ScoredProvider (match results) now includes optional doctor and language_support fields when DOCTORS_IN_MATCHING feature flag is enabled
  • doctor: lead doctor profile, procedure stats, language concordance tier, match reasoning
  • language_support: concordance tier, interpreter availability, consent form language

New Error Codes

Code HTTP Description
DOCTOR_NOT_FOUND_001 404 Doctor ID or slug does not exist
DOCTOR_VALIDATION_001 422 Required doctor field missing or invalid
DOCTOR_DUPLICATE_001 409 Doctor with same name + provider + specialty already exists
DOCTOR_PROCEDURE_EXISTS_001 409 Doctor already has this procedure_code assigned

Feature Flags

  • DOCTORS_IN_MATCHING — When ON, match results include doctor-level data. Default: OFF in production.

v1.25B — Document Matching via Embeddings (Session 25B)

  • Document checklist now uses embedding-based matching (Qdrant requirement_embeddings)
  • Keyword matching retained as fallback

v1.25 — UI/UX Overhaul (Session 25)

  • No API changes (frontend only)

v1.24 — Agent Guardrails (Session 24)

  • Input classifier on /chat endpoint (8 categories)
  • Output validator on agent responses (12 forbidden patterns)
  • File validation at presign time

v1.23 — Document Checklist + Cases (Session 23)

  • /api/v1/cases/{id}/document-checklist endpoint
  • /api/v1/cases/ CRUD endpoints
  • Procedure requirements table and seeding

v1.17 — Core Platform (Session 17)

  • Initial 17-table schema
  • Patient, Provider, FHIR, Consent, Match, Document endpoints
  • Matching engine with weighted scoring v1