Architecture overview
Docket is built around three core ideas: one binary plus one Postgres, an event-sourced ticket core, and AI that can be turned off without losing ticketing.
Single binary, one Postgres
Section titled “Single binary, one Postgres”Everything runs in a single Go binary backed by a single PostgreSQL database:
- HTTP API and React SPA
- Background jobs and durable workflows (DBOS Transact)
- AI agent runs and checkpoints
- Embeddings and semantic search (pgvector)
- Migrations (goose, embedded via
go:embed)
No Redis, no separate queue, no external vector store. The frontend is built with Vite and embedded into the Go binary; the database extension pgvector is enabled by the first migration.
This shape is a hard invariant. If a feature seems to require a sidecar service, that’s a signal to redesign rather than to widen the dependency surface.
Horizontal scale, stateless instances
Section titled “Horizontal scale, stateless instances”Any instance can serve any request and resume any in-flight workflow. There’s no leader, no in-memory shared state, no sticky sessions. Scaling out means running N copies behind a load balancer pointed at the same database; PostgreSQL LISTEN/NOTIFY is used for cache invalidation across instances.
DBOS workflows are deduplicated across instances by workflow ID, so scheduled jobs run exactly once even when every instance ticks at the same moment.
Event-sourced ticket core
Section titled “Event-sourced ticket core”Every mutation to a ticket flows through a single function — apply(event). That function, in one transaction:
- Validates the event against current ticket state.
- Inserts a row in
ticket_events(the append-only spine). - Inserts a row in the matching
ticket_events_<type>detail table. - Updates the
ticketsprojection (andticket_tags, attachments, SLA bookkeeping as needed).
Direct writes to tickets are forbidden. Spine rows always have a matching detail row. The (ticket_id, seq) unique constraint provides compare-and-swap concurrency control — apply retries internally on stale-state conflicts, so callers never see seq.
Because the spine is the source of truth, the projection can always be rebuilt from it via docket rebuild-tickets. See CLI reference for that command and Data model for schema specifics.
AI is optional, by design
Section titled “AI is optional, by design”Docket’s AI agents (triage, assist, resolver) are implemented on top of the Eino framework and run as DBOS workflows. Each agent run pins a spec_hash (recorded on the run and in its workflow input; the DBOS application_version is a workflow-compatibility version shared by every build, so any instance of a new release recovers runs the previous release left in flight) and has a hard max_run_duration ceiling.
Critically, ticketing works end-to-end with AI disabled. There are no hard dependencies from ticket-core packages into AI packages. Setting [ai].enabled = false (or omitting the section) turns off agent enqueueing entirely; workers idle, the system continues.
See AI framework for how spec registry, pause/wake, and deadlines fit together.
Stack at a glance
Section titled “Stack at a glance”| Concern | Choice |
|---|---|
| Language | Go 1.26.1+ |
| HTTP router | Chi |
| DB access | sqlc + pgx/v5 |
| Migrations | goose |
| Background jobs / workflows | DBOS Transact Go |
| AI framework | Eino |
| Embeddings | pgvector (HNSW indexes) |
| Frontend | React 19 + TanStack Router/Query + Tailwind v4 |
| Authn | OIDC + SCIM 2.0 + local-fallback |
| Authz | Casbin (RBAC with domains) |
| Telemetry | OpenTelemetry |