Developer docs

Run it, read it, test it.

How to run ClinicalBrief locally, what the API exposes, and the checks every change has to pass. Commands are copied from the project README.

Overview

Two apps, one repository.

  • clinicalbrief-backend

    FastAPI on Python 3.12 only. Ingestion adapters, the AI pipeline, search, FHIR export, the REST API with role and patient checks, and the SQL migrations (schema and row-level security, the source of truth for Postgres).

  • clinicalbrief-frontend

    Next.js 16 with Supabase Auth. It reads clinical data only through the API, sending the session token as a Bearer header.

Local development can use SQLite (clinicalbrief_dev.db), created from the ORM. It has no migration path: after model changes, delete it and re-import. For the architecture story, see About ClinicalBrief.

Run it locally

From clone to a running app.

Prerequisites

  • Python 3.12 (commands use py -3.12)
  • Node.js with npm
  • A Supabase project, for sign-in and the Postgres database
  • Optional: Ollama with qwen3.5, for the Copilot

1. Database

In the Supabase SQL editor, run every file in clinicalbrief-backend/migrations/ in order. There is no 0006.

2. Environment

Copy clinicalbrief-backend/.env.example to .env and clinicalbrief-frontend/.env.example to .env.local, then fill them in. Never put a service-role or secret key in the frontend.

Backend .env
  • DATABASE_URL
  • SUPABASE_URL
  • JWT_SECRET
  • CORS_ORIGINS
  • USE_MOCK_MODELS
  • LLM_BASE_URL
  • LLM_MODEL
Frontend .env.local
  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
  • NEXT_PUBLIC_API_URL
  • NEXT_PUBLIC_TURNSTILE_SITE_KEY (optional, public demo CAPTCHA)

3. Remote database opt-in

When DATABASE_URL points at Supabase, the backend and CLI refuse to start unless CLINICALBRIEF_ALLOW_REMOTE_DB=1 is set for that process. Keep it out of .env, so scripts and tests can never write to live data by default. CLI write commands print the target database first.

$env:CLINICALBRIEF_ALLOW_REMOTE_DB="1"   # PowerShell, current process only

4. Backend

cd clinicalbrief-backendpy -3.12 -m pip install -r requirements-dev.txtpy -3.12 -m app.cli import synthea ../data/raw/synthea-fhirpy -3.12 -m uvicorn app.main:app --reload --no-access-log

--no-access-log is deliberate: uvicorn's access log prints full URLs, which contain patient ids. The app records route templates, timing and a request id per call instead (System health).

FastAPI's interactive API docs are at /docs on the backend port when EXPOSE_API_DOCS=true (the local launch config sets it; it is off in production). The root endpoint reports docs_url. requirements-ml.txt adds the transformer stack (several GB) for USE_MOCK_MODELS=false; that mode exists but is untested.

5. Frontend

cd clinicalbrief-frontendnpm installnpm run dev

6. First user

Create a user in Supabase Auth. New accounts start as pending with no access (every endpoint answers 403). Grant a role from the CLI:

py -3.12 -m app.cli set-role you@example.com admin

7. Copilot (optional)

Install Ollama, pull the model, and set LLM_MODEL / LLM_BASE_URL in .env if needed. Only loopback model URLs and locally run models are accepted. Without the model, the Copilot falls back to labelled rule-based answers.

ollama pull qwen3.5py -3.12 -m app.cli ask <patient_id> "<question>"
Data

The import CLI.

Data comes from Synthea (synthetic, FHIR R4). data/raw/ is git-ignored.

py -3.12 -m app.cli discover <path>                         # what's in a folder, what will be imported or skippedpy -3.12 -m app.cli import synthea <path>         [--dry-run] [--limit N]py -3.12 -m app.cli runs                                    # import history with counts and rejected recordspy -3.12 -m app.cli process-notes --limit N                 # run the AI pipeline on unprocessed notespy -3.12 -m app.cli set-role <email> <role>py -3.12 -m app.cli flag-demo [--count 10] [--apply]        # choose the public demo patients (dry run by default)
  • Record ids are derived from (source_system, table, source_id), so re-importing updates rows instead of duplicating them.
  • Invalid source records are skipped and written to import_errors with a reason; the rest continues.
  • An unexpected failure rolls back every data write and marks the run failed.
  • Source values are never invented: unknown birth dates, statuses and codes stay NULL.
API overview

Main routes.

All paths are under /api/v1 and need a Supabase Bearer token. “Patient-scoped” means clinicians reach only patients assigned to them; “not yours” and “doesn't exist” both return 404, so ids can't be probed. Roles come from require_roles(...) in app/api/v1/deps.py.

MethodPathPurposeRoles
GET/auth/meCurrent user's profile and roleAny signed-in user with a role
GET/patientsList accessible patientsAny active role, patient-scoped
POST/patientsCreate a patientadmin, clinician, consultant
GET/patients/{id}Patient summaryAny active role, patient-scoped
GET/patients/{id}/recordFull structured record (audited read)Any active role, patient-scoped
GET/patients/{id}/riskHeuristic risk score, not clinically validatedAny active role, patient-scoped
GET/patients/{id}/graphPatient-centred knowledge graphAny active role, patient-scoped
GET/patients/governance/statsGovernance countsadmin, auditor
POST/documents/uploadUpload a UTF-8 .txt note (max 5 MB)admin, clinician, consultant
GET/documents/patient/{id}A patient's notesAny active role, patient-scoped
POST/documents/{id}/processRun the AI pipeline on a noteadmin, clinician, consultant
GET/documents/{id}/statusPipeline progressAny active role, patient-scoped
GET/documents/{id}/insightsAI output and evidence sentences (audited read)Any active role, patient-scoped
GET/documents/{id}/redactedNote text with regex redaction; researchers get redacted text onlyAny active role, patient-scoped
GET/documents/compare/{id}Earliest vs latest note, reviewed entities onlyAny active role, patient-scoped
POST/documents/entities/{id}/reviewApprove, reject or edit one AI findingadmin, clinician, consultant, coder
GET/review/queueCross-patient review queueadmin, clinician, consultant, coder
POST/review/bulkBulk review decisionsadmin, clinician, consultant, coder
GET/review/historyReview historyadmin, clinician, consultant, coder, auditor
GET/review/statsReview statistics for governanceadmin, auditor
GET/ops/healthLive checks: database, local LLM, search indexadmin, auditor
GET/ops/metricsRequest counts, error rates and latency per route template (?window= seconds)admin, auditor
POST/search/recordsPostgres full-text search over notes and the structured recordAny active role, patient-scoped
POST/search/queryIn-memory note search (prototype)Any active role, patient-scoped
POST/search/qaQuestion answering over one document (prototype)Any active role, patient-scoped
GET/copilot/conversations/{patient_id}List Copilot sessionsAny active role, patient-scoped
POST/copilot/conversations/{patient_id}Start a Copilot sessionAny active role, patient-scoped
DELETE/copilot/conversations/{id}Hard-delete a session (owner only)Any active role, patient-scoped
POST/copilot/chat/{conversation_id}Ask a grounded question (audited)Any active role, patient-scoped
GET/fhir/export/{patient_id}FHIR R4 transaction bundle (audited)admin, clinician, consultant, coder
GET/audit/logsAudit log, pagedadmin, auditor
GET/importsImport run historyadmin, auditor
GET/imports/{run_id}/errorsRejected records for one runadmin, auditor
GET/dashboard/layoutLoad the user's dashboard layoutAny signed-in user with a role
POST/dashboard/layoutSave the user's dashboard layoutAny signed-in user with a role

Responses may carry X-Total-Count (full size of a limited list), X-Pending-Entities (AI findings awaiting review) and X-FHIR-Validation (the export check result).

Security model

Six roles and a demo, enforced twice.

The API enforces roles, and Postgres row-level security mirrors them for anything a browser could call directly with the publishable key: clinical tables are read-only to clients, users cannot change their own role, and clients cannot write audit entries. All writes go through the API. Roles are never taken from signup metadata.

RolePatients visibleCan write
adminalleverything; role changes and imports are done via the CLI
clinicianonly patients assigned to themcreate patients, upload notes, review AI output
consultantallcreate patients, upload notes, review AI output
coderallreview/edit AI output (codes)
auditorall (read-only)nothing; can read audit log, governance and import history
researcherall (read-only)nothing (a de-identified view is planned)
demoonly patients flagged for the public demo (read-only)nothing; Copilot only, private sessions. Anonymous sign-in
Quality gates

Tests and hooks.

The backend suite runs on SQLite, and again on a local Postgres (pgserver) with the real migrations and RLS applied, testing as the authenticated and anon roles. Tests use a fake model and never call Ollama.

cd clinicalbrief-backendpy -3.12 -m pytest tests               # SQLiteTEST_PG=1 py -3.12 -m pytest tests     # local Postgres (pgserver) with the real migrations and RLS applied

Frontend checks:

npx tsc --noEmitnpm run buildnpm audit --omit=dev

Git hooks: pre-commit and pre-push run a secret scan (database URLs with a real password, Supabase secret keys, private keys). pre-push then runs the Postgres suite, failing if any test is skipped so the RLS and schema-drift guards always run, and the frontend type check. Enable them once per clone:

git config core.hooksPath .githooks

No linter is configured yet, for the backend or the frontend.

Conventions

How the project is kept honest.

  • Honesty labels

    Every feature is marked Implemented, Prototype or Not built in the README.

  • No fabricated metrics

    No accuracy figure is shown anywhere until it is measured on a labelled set. Rates on the governance page are shares of human decisions, not accuracy.

  • Human review first

    AI output is never treated as fact until a person approves or edits it. The graph, FHIR export, Copilot structured answers and risk score use reviewed entities only.

The schema in migrations/*.sql is the source of truth; the ORM must match it, and a test fails on any drift.

Known gaps

What is not done yet.

Known gaps and planned work are tracked in one place each, so they are not repeated here.