2026-06-20 11:50:44 -06:00

Cyclone

A self-hosted EDI claims management suite for a single billing office. Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 and 005010X221A1), with a local-only FastAPI backend and a React UI for browsing, filtering, and inspecting the parsed data.

Local-only on purpose: binds to 127.0.0.1, no auth, no internet exposure. Built for one operator, one machine, one trading partner (Colorado Medicaid, currently).

Install

# Backend (Python 3.11+)
cd backend
python -m venv .venv
.venv/bin/pip install -e '.[dev]'

# Frontend (Node 20+)
cd ..
npm install

Dev

Two terminals:

# Terminal 1 — backend
cd backend
.venv/bin/python -m cyclone serve
# (defaults to 127.0.0.1:8000; override with CYCLONE_PORT=...; reload with CYCLONE_RELOAD=1)

# Terminal 2 — frontend
npm run dev
# (Vite on http://localhost:5173)

Then open http://localhost:5173. Drop an .txt 837P or 835 file on the Upload page; navigate to Claims, Remittances, Providers, or Activity to see the parsed data.

The frontend reads its backend URL from VITE_API_BASE_URL (default empty). Create a .env.local at the repo root with:

VITE_API_BASE_URL=http://127.0.0.1:8000

Without that, the UI falls back to its in-memory sample store via the existing data adapter (parses are disabled).

Test

# Backend
cd backend && .venv/bin/pytest

# Frontend type-check + build
npm run typecheck
npm run build

# Frontend unit tests
npm test

Persistence

Parsed batches, claims, remittances, matches, and activity events are stored in a SQLite file at ~/.local/share/cyclone/cyclone.db by default. The directory is auto-created on first run.

To use a different location, set CYCLONE_DB_URL:

export CYCLONE_DB_URL=sqlite:///path/to/cyclone.db
# or
export CYCLONE_DB_URL=postgresql://user:pass@host:5432/cyclone

Backup

sqlite3 ~/.local/share/cyclone/cyclone.db ".backup /path/to/backup.db"

This is safe to run while the backend is running (uses SQLite's online backup API).

Project layout

.
├── backend/
│   ├── src/cyclone/
│   │   ├── api.py             # FastAPI app, GET + parse routes
│   │   ├── store.py           # InMemoryStore, mappers
│   │   ├── __main__.py        # `python -m cyclone serve`
│   │   ├── cli.py             # click CLI
│   │   └── parsers/           # X12 tokenizer, models, validator, writers
│   └── tests/
│       ├── fixtures/          # co_medicaid_*.txt, minimal_*.txt
│       ├── test_api.py        # parse-837/835 round-trip
│       ├── test_api_gets.py   # 6 GET endpoints
│       ├── test_store.py      # store + mappers + iterators
│       ├── test_api_streaming.py
│       └── test_api_parse_persists.py
├── src/                       # React + Vite + TypeScript UI
│   ├── components/
│   │   └── ui/                # Skeleton, EmptyState, ErrorState, FilterChips, Pagination, …
│   ├── pages/                 # Claims, Remittances, Providers, Activity, Upload
│   ├── hooks/                 # useBatches, useClaims, useRemittances, useProviders, useActivity, useParse
│   ├── lib/                   # api.ts (6 GET + parse837/parse835/health), format.ts, utils.ts
│   ├── store/                 # zustand sample-data + parsed-batches store
│   └── types/                 # shared TS types
├── docs/
│   ├── reference/             # condensed 837P/835/X12/CO Medicaid notes
│   └── superpowers/plans/     # implementation plan
├── tailwind.config.js         # shimmer, scan, row-flash keyframes
└── package.json

Roadmap

Sub-projects 2 and 3 are shipped. Next up:

  • Sub-project 3 (shipped) — More 837P/835 features.
    • 837P validation rules: R034 enforces REF*G1 on frequency-code 7/8 claims; R035 enforces BHT06 transaction-type-code is in the allowed set per payer config.
    • 835 CAS deep-parsing: every CAS adjustment now surfaces with its CARC reason code + label (e.g. CO-29: The time limit for filing has expired), surfaced via GET /api/remittances/{id} and rendered as an expansion row on the Remittances page.
    • 999 ACK transaction set: full inbound parser + outbound serializer. Auto-generated on 837 ingest when ?ack=true is passed to POST /api/parse-837. Persisted to the acks table, browsable on a new /acks page, and downloadable as the regenerated raw 999 text.
    • 270/271 eligibility (API-only): POST /api/eligibility/request builds a 270 from a JSON payload (subscriber, provider, payer, service type); POST /api/eligibility/parse-271 ingests the response and returns structured coverage_benefits.
  • Sub-project 4 — Frontend features. Per-claim detail drawer, batch diff view, real-time streaming of NDJSON GET responses, advanced filters (date range, multi-status, saved filter sets), keyboard-driven navigation.

SP3 endpoints

  • POST /api/parse-837?ack=true — existing 837 parse, plus optional auto-generated 999 ACK persisted alongside the batch.
  • POST /api/parse-999 — parse an inbound 999 ACK and persist it.
  • GET /api/acks — list ACKs.
  • GET /api/acks/{id} — ACK detail, including the regenerated raw_999_text.
  • POST /api/eligibility/request — build a 270 from JSON.
  • POST /api/eligibility/parse-271 — ingest a 271 and return parsed coverage benefits.

The UI gains a new Acks page (sidebar entry) that lists persisted ACKs and lets you download the regenerated 999 text.

License

No license file yet; this is internal-use software. Add a LICENSE file when one is decided.

S
Description
No description provided
Readme 8.6 MiB
Languages
Python 62%
TypeScript 37.1%
CSS 0.4%
JavaScript 0.3%
Shell 0.1%