Architecture
Exponential is an event-sourced issue tracker that stores its data in a git-tracked append-only log. A single Go binary ships the CLI, a web UI, an HTTP API, and an MCP server.
Event sourcing
All mutations are recorded as events. There is no mutable state on disk. Each event carries the issue ID, its type and payload, and who made it:
{"id": "myapp-a1b2c3", "type": "UPDATE", "payload": {"status": "DOING", "assignee": "Alice <alice@example.com>"},
"created_at": "2026-10-09T15:23:35Z", "created_by": "claude-code/2.1 <agent@mcp>",
"on_behalf_of": "Alice <alice@example.com>", "source": "mcp"}
created_by is the actor, on_behalf_of the person an agent works for, and source the channel (cli, mcp, or web). The event types and their payloads:
{"type": "CREATE", "payload": {"title": "...", "status": "BACKLOG", "labels": ["feature"]}}
{"type": "UPDATE", "payload": {"status": "DOING", "assignee": "Alice <alice@example.com>"}}
{"type": "COMMENT", "payload": {"id": "cmt-...", "text": "markdown body"}}
{"type": "DELETE", "payload": {"reason": "duplicate"}}
{"type": "MERGE", "payload": {"branch": "myapp-a1b2c3-login", "base_sha": "...", "merge_sha": "...", "strategy": "squash"}}
{"type": "ARTIFACT", "payload": {"artifact_type": "spec", "filename": "spec.md", "action": "created"}}
Events are stored as one JSON object per line (JSONL) in .xpo/issues.db. Several writers can run at once — MCP servers, the CLI, and the web server — so every append, collapse, and archive holds an exclusive lock on .xpo/events.lock, and each line is written in a single call. Readers ignore a torn final line from an append in progress. Git merge conflicts stay limited to the tail of the file.
Projection
Current state is derived by replaying the event log. Each event type has a deterministic effect — CREATE initializes, UPDATE patches, DELETE soft-deletes, COMMENT appends, MERGE records metadata. Post-processing applies config-driven rules: parent status inference, cycle rollover, estimate aggregation.
Transport abstraction
All operations go through a Transport interface:
- LocalTransport reads and writes
issues.dbdirectly. Resolves IDs flexibly — exact, prefix, or substring match. - Client wraps a Transport and adds git-specific operations (branching, merging, review).
xpo is local-only: there is no server to sync with. The issue data travels with your code through git, and .xpo/issues.db is marked merge=union in .gitattributes, so teammates get the latest copy when they pull.
Storage
| Path | Git-tracked | Purpose |
|---|---|---|
.xpo/issues.db | Yes | Active event log (JSONL) |
.xpo/archive.db | Yes | Archived events |
.xpo/config.yaml | Yes | Project configuration |
.xpo/artifacts/<id>/ | Yes | Specs, walkthroughs, and other files attached to an issue |
.xpo/worktrees/ | No | One git worktree per started issue |
.xpo/chats/ | No | Your agent chats, with their attachments |
.xpo/drafts/ | No | Your unsaved issue drafts |
.xpo/events.lock | No | Lock serializing writes to the event log |
.xpo/git.lock | No | Lock serializing xpo's git operations |
Web UI internals
The server maintains a read-through projection cache. The web UI persists mutations automatically. To keep the log compact, consecutive uncommitted edits to the same issue by the same actor are collapsed into one UPDATE, and no-op edits are pruned. Edits are never merged across actors or past a newer event on the issue, and the merged event moves to the end of the log so timestamps always follow file order. xpo doctor reports any lines whose created_at goes backwards.
Real-time updates use Server-Sent Events (SSE). Each board tab holds one stream, GET /api/events, that carries issue, draft and chat changes and the output of every open chat, with a 25-second keepalive. After a reconnect (sleep, network drop, board restart), the tab refetches what changed and replays each open chat from where it left off.
Data model versioning
| Version | Change |
|---|---|
| v1 | Initial schema (typed issues: TASK, BUG, EPIC) |
| v2 | Unified Issue type with labels, assignee, estimation |
| v3 | Globally consistent sort order keys |
xpo migrate appends corrective events — existing events are never modified.