Skip to content

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.

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.

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.

Every mutation to a ticket flows through a single function — apply(event). That function, in one transaction:

  1. Validates the event against current ticket state.
  2. Inserts a row in ticket_events (the append-only spine).
  3. Inserts a row in the matching ticket_events_<type> detail table.
  4. Updates the tickets projection (and ticket_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.

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.

ConcernChoice
LanguageGo 1.26.1+
HTTP routerChi
DB accesssqlc + pgx/v5
Migrationsgoose
Background jobs / workflowsDBOS Transact Go
AI frameworkEino
Embeddingspgvector (HNSW indexes)
FrontendReact 19 + TanStack Router/Query + Tailwind v4
AuthnOIDC + SCIM 2.0 + local-fallback
AuthzCasbin (RBAC with domains)
TelemetryOpenTelemetry