DocsReferenceArchitecture
Back
Reference

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.db directly. 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

PathGit-trackedPurpose
.xpo/issues.dbYesActive event log (JSONL)
.xpo/archive.dbYesArchived events
.xpo/config.yamlYesProject configuration
.xpo/artifacts/<id>/YesSpecs, walkthroughs, and other files attached to an issue
.xpo/worktrees/NoOne git worktree per started issue
.xpo/chats/NoYour agent chats, with their attachments
.xpo/drafts/NoYour unsaved issue drafts
.xpo/events.lockNoLock serializing writes to the event log
.xpo/git.lockNoLock 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

VersionChange
v1Initial schema (typed issues: TASK, BUG, EPIC)
v2Unified Issue type with labels, assignee, estimation
v3Globally consistent sort order keys

xpo migrate appends corrective events — existing events are never modified.