Architecture
Upliftr is AI debugging agents that debug your app: you chat with them in plain English, they drive a real browser to reproduce the problem, and they root-cause the failure to the exact backend line. They are non-destructive by design, read-only access (e.g. read-only log credentials), running in your own environment, so they investigate and diagnose without ever mutating your code, data, or infrastructure. Under the hood it is two standalone apps over a shared engine, plus an MCP server and a CLI that reuse the same engine: a FastAPI backend (the JSON API + the execution engine) and a Vite + React dashboard where you chat with the agents.
Cursor / Claude Code ─► MCP server ─┐
CI / terminal ───────► CLI ─────────┤ (both import the engine directly)
│
Browser ──HTTP──► Dashboard (Vite/React SPA)
│ fetch /api/v1
▼
Backend (FastAPI)
api/v1/routes ─► services ─► store (file repos)
│ │
│ └─ engine ── Claude (brain) ─► Playwright (hands) ─► REAL browser
▼ │
suites · runs · issues · ▼ network capture → trace_id
settings · generate · failure → server-log correlation → Claude → root cause
analytics · export · healthClaude is the brain, Playwright is the hands
The execution core (engine/pw_agent.py + engine/pw.py) runs plain-English steps in a real browser on any OS, Windows, macOS, or Linux, headless or headed. No AppleScript, no Quartz, no OS permission prompts:
- Claude is the brain. For each step it reads the live DOM and turns the plain-English intent into concrete browser actions.
- Playwright is the universal hands. It launches and drives a headless Chromium that executes those actions, and captures the network so a UI failure still yields a
trace_id.
The runner is Playwright-only and cross-platform (Windows / macOS / Linux, headless or headed), and is what the CLI, MCP server, and CI integrations all use.
The step toolbox
Each step is driven by a small, explicit toolset the model calls. The DOM snapshot pierces open shadow DOM and iframes and flags disabled/read-only fields, so these work across modern component libraries:
| Tool | What it does |
|---|---|
read_dom | Read the page (title, URL, visible text, numbered #ref elements) across the main frame, iframes, and open shadow DOM. |
goto | Navigate to a path or URL. |
click_element | Click an element by #ref (recovers from overlays / interception). |
set_field | Set an input / textarea / select by #ref; fails fast on a disabled field. |
press_key / type_text | Press a key or chord; type into the focused element. |
scroll / hover | Reveal lazy-loaded content; open hover-only menus. |
screenshot / click_xy | Vision fallback: look at the page and act by coordinates when the DOM can't express something (canvas UIs, unresolved elements). |
create_file / upload_file | Create a file with given content and upload it into a file input, no pre-made fixture needed. |
done | The step's intent is achieved. |
Steps are a guide to intent, not literal commands: the runner self-heals to the live UI (a renamed/moved control, an unexpected dialog) and only fails when the intent genuinely can't be met. The autonomous (goal) mode adds remember (carry a value across steps) and check_server_logs (corroborate the UI against the backend).
Because the model both creates and uploads files, an import/upload flow needs no pre-made fixture file: "Create a small CSV and upload it as your import" just works.
Backend (backend/app)
Layered so each concern is swappable and testable in isolation.
| Layer | Module | Responsibility |
|---|---|---|
| core | core/ | config.py (pydantic-settings + data paths), errors.py, logging.py. |
| schemas | schemas/models.py | Pydantic request models. |
| store | store/db.py, store/models.py, store/repos.py | SQLAlchemy-backed repositories, suites, runs, issues, plans, settings, users/API tokens, behind a clean interface. SQLite by default, Postgres in production. |
| services | services/ | runner (a durable, parallel worker pool that claims queued runs and executes them, streaming progress + persisting), auth (users, sessions, API tokens), and issues (the native File-to-Upliftr tracker + dedup, and dispatch to GitHub / GitLab / Jira). |
| api/v1 | api/v1/routes/ | The HTTP surface, see below. |
| engine | engine/ | The execution core. |
The engine modules
| Module | Role |
|---|---|
pw.py | The Playwright Driver, launch, navigate, page state, network capture, root cause. |
pw_agent.py | The cross-platform runner: per-step LLM loop + suite executor (the default path). |
e2e.py | Suite loading, ${var} substitution, fixtures, deterministic checks, model adjudication. |
serverlogs.py | Server-log correlation + LLM synthesis: trace_id → backend error + cause + fix. |
generate.py | URL → suite generation (crawl, read structure, propose a suite). |
export_playwright.py | Compile a suite to a portable Playwright .spec.ts. |
analytics.py | Flake / run analytics over persisted runs. |
reports.py | report.json · junit.xml · report.html + provider-agnostic issues (fingerprinted). |
trackers.py | Live issue posting to GitHub / GitLab / Jira, deduped by fingerprint. |
dom.py | The shared DOM-read JS (interactive elements across frames + open shadow DOM). |
cdp.py | Chrome DevTools Protocol: traceparent / network capture for trace_id correlation. |
probe.py, egress.py | Infra probes (tcp / http / dns / tls) and the SSRF egress guard. |
The HTTP surface (/api/v1)
health · suites (CRUD + {name}/export) · runs (queue, inspect, queue state) · issues (the native tracker: list, transition, comment) · settings (secret-masked) · generate (URL → suite) · analytics (flake/run trends). Full details in the API reference.
Storage
Structured data lives in a SQL database via SQLAlchemy, SQLite by default (a single file under data/), Postgres in production (UPLIFTR_DATABASE_URL). The tables: suites, runs, issues, plans, settings (secret values encrypted at rest), and users / api_tokens. Tables are created on startup; on first boot any legacy file-based data is imported once.
Run artifacts stay on the filesystem under data/ (served at /api/v1/artifacts):
data/
upliftr.db the default SQLite database (suites, runs, issues, plans, settings, users)
artifacts/ report.json / junit.xml / report.html / per-step screenshots / issues
baselines/ visual-diff baselines (screenshot_region checks)The repository interface in store/repos.py keeps the same method signatures whether the backing store is SQLite or Postgres, so the services and API above it are unchanged.
Frontend (frontend/src)
A Vite + React + Tailwind SPA with TanStack Query as the data layer. It talks only to /api/v1, so the dashboard and any other client (scripts, CI, the MCP server) share exactly the same contract.
How a run flows
- The dashboard (or an API client)
POSTs to/runswith a suite (or several). Each run is persisted as a queued row, a durable queue that survives a restart. - The
runnerservice runs a worker pool (UPLIFTR_MAX_CONCURRENT_RUNS, default 2) that claims queued rows transactionally and executes them in parallel, streaming progress. A crash mid-run is reconciled on the next startup. - The engine opens one Playwright browser for the whole suite; Claude drives each step; assertions are adjudicated (deterministically where a
checkexists). - On a failure, the network buffer yields a
trace_id; if a log backend is configured,serverlogs.pycorrelates it and Claude synthesizes the backend root cause. reports.pywrites the artifacts and builds a deduped issue;services/issuesortrackers.pyfiles it. Results persist tostore, and the dashboard renders live.
Continue to Runs & assertions.