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.
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.
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.
- DATABASE_URL
- SUPABASE_URL
- JWT_SECRET
- CORS_ORIGINS
- USE_MOCK_MODELS
- LLM_BASE_URL
- LLM_MODEL
- 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 only4. 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 dev6. 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 admin7. 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>"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_errorswith 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.
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.
| Method | Path | Purpose | Roles |
|---|---|---|---|
| GET | /auth/me | Current user's profile and role | Any signed-in user with a role |
| GET | /patients | List accessible patients | Any active role, patient-scoped |
| POST | /patients | Create a patient | admin, clinician, consultant |
| GET | /patients/{id} | Patient summary | Any active role, patient-scoped |
| GET | /patients/{id}/record | Full structured record (audited read) | Any active role, patient-scoped |
| GET | /patients/{id}/risk | Heuristic risk score, not clinically validated | Any active role, patient-scoped |
| GET | /patients/{id}/graph | Patient-centred knowledge graph | Any active role, patient-scoped |
| GET | /patients/governance/stats | Governance counts | admin, auditor |
| POST | /documents/upload | Upload a UTF-8 .txt note (max 5 MB) | admin, clinician, consultant |
| GET | /documents/patient/{id} | A patient's notes | Any active role, patient-scoped |
| POST | /documents/{id}/process | Run the AI pipeline on a note | admin, clinician, consultant |
| GET | /documents/{id}/status | Pipeline progress | Any active role, patient-scoped |
| GET | /documents/{id}/insights | AI output and evidence sentences (audited read) | Any active role, patient-scoped |
| GET | /documents/{id}/redacted | Note text with regex redaction; researchers get redacted text only | Any active role, patient-scoped |
| GET | /documents/compare/{id} | Earliest vs latest note, reviewed entities only | Any active role, patient-scoped |
| POST | /documents/entities/{id}/review | Approve, reject or edit one AI finding | admin, clinician, consultant, coder |
| GET | /review/queue | Cross-patient review queue | admin, clinician, consultant, coder |
| POST | /review/bulk | Bulk review decisions | admin, clinician, consultant, coder |
| GET | /review/history | Review history | admin, clinician, consultant, coder, auditor |
| GET | /review/stats | Review statistics for governance | admin, auditor |
| GET | /ops/health | Live checks: database, local LLM, search index | admin, auditor |
| GET | /ops/metrics | Request counts, error rates and latency per route template (?window= seconds) | admin, auditor |
| POST | /search/records | Postgres full-text search over notes and the structured record | Any active role, patient-scoped |
| POST | /search/query | In-memory note search (prototype) | Any active role, patient-scoped |
| POST | /search/qa | Question answering over one document (prototype) | Any active role, patient-scoped |
| GET | /copilot/conversations/{patient_id} | List Copilot sessions | Any active role, patient-scoped |
| POST | /copilot/conversations/{patient_id} | Start a Copilot session | Any 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/logs | Audit log, paged | admin, auditor |
| GET | /imports | Import run history | admin, auditor |
| GET | /imports/{run_id}/errors | Rejected records for one run | admin, auditor |
| GET | /dashboard/layout | Load the user's dashboard layout | Any signed-in user with a role |
| POST | /dashboard/layout | Save the user's dashboard layout | Any 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).
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.
| Role | Patients visible | Can write |
|---|---|---|
| admin | all | everything; role changes and imports are done via the CLI |
| clinician | only patients assigned to them | create patients, upload notes, review AI output |
| consultant | all | create patients, upload notes, review AI output |
| coder | all | review/edit AI output (codes) |
| auditor | all (read-only) | nothing; can read audit log, governance and import history |
| researcher | all (read-only) | nothing (a de-identified view is planned) |
| demo | only patients flagged for the public demo (read-only) | nothing; Copilot only, private sessions. Anonymous sign-in |
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 appliedFrontend checks:
npx tsc --noEmitnpm run buildnpm audit --omit=devGit 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 .githooksNo linter is configured yet, for the backend or the frontend.
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.
What is not done yet.
Known gaps and planned work are tracked in one place each, so they are not repeated here.