Architecture

System context

Rabbit ships as three binaries from one module (github.com/holmes89/rabbit):

  • cmd/api โ€” the ConnectRPC server. h2c HTTP/2 handler over a net/http mux, backed by Postgres (pgxpool) and Kafka (via github.com/holmes89/archaea/kafka). Runs goose migrations (lib/repo/migrations) on boot, then serves ProjectService, SprintService, TicketService, and DesignService. Also exposes GET /health and GET /info.
  • cmd/worker โ€” a Kafka consumer process. Upserts sprint/ticket/design events into Postgres, mirrors ticket bodies to markdown files on disk, and bridges to two other services in my ecosystem: narwhal doc approvals become Rabbit designs, and narwhal products become Rabbit projects.
  • cmd/rabbit โ€” a Cobra CLI (project, sprint, ticket, design, sync, dump, migrate) that talks to the API over ConnectRPC. Ticket and design bodies are edited via $EDITOR (lib/rabbit/editor), matching the flow of writing a ticket by hand.
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   CLI (rabbit) โ”€โ”ค  cmd/api   โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚  Postgres     โ”‚
                 โ”‚ ConnectRPC โ”‚        โ”‚ (pgx, goose)  โ”‚
                 โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ”‚ publishes Sprint/Ticket/Design
                       โ–ผ
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚   Kafka     โ”‚
                 โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ–ผ
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚ cmd/worker  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ tickets/*.md  โ”‚ (mirrored files)
                 โ”‚ (consumers) โ”‚        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                 โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ”‚ consumes narwhal DesignDoc / Product topics
                       โ”‚ publishes magpie Resource events
                       โ–ผ
              narwhal, magpie (other services)

Ticket / sprint data model

Everything is defined in schemas/rabbit/v1/rabbit.proto and generated into lib/schemas/rabbit/v1.

  • Project โ€” uuid, name, prefix, description, next_seq counters (next_ticket_seq, next_design_seq in the DB), optional meerkat_project_uuid and product_uuids linking it to other planning systems.
  • Sprint โ€” belongs to a project, has a sequential index (0, 1, 2, … within the project) instead of a human-readable ID, and a SprintStatus: PLANNED โ†’ ACTIVE โ†’ COMPLETE.
  • Ticket โ€” belongs to a project and (optionally) a sprint and a design. Has TicketType (FEATURE / BUG / TASK), TicketStatus (TODO โ†’ IN_PROGRESS โ†’ TESTING โ†’ DONE, plus BLOCKED, plus DRAFT for proposals awaiting review โ€” see below), and Priority (LOW/MEDIUM/HIGH). repo names the actual repository the work belongs to (e.g. beaver, narwhal, joel.holmes.haus) โ€” this is how Rabbit routes planning to the right codebase without owning that code itself. depends_on is a list of other ticket UUIDs; blocked_by_dependency is computed on read (true if any dependency isn’t DONE) and is advisory only โ€” it never blocks a status transition. A ticket created with CreateTicketRequest.draft = true starts in DRAFT instead of TODO; DRAFT tickets are omitted from ListTickets unless status is set to DRAFT explicitly, and are “approved” by moving them to TODO (SetTicketStatus, or rabbit ticket approve <uuid>).
  • Design โ€” a spec: title/body/description plus a list of acceptance_criteria. SpawnTickets turns each acceptance-criteria line into a real TASK ticket linked back to the design via design_uuid. Designs can originate from an approved narwhal DesignDoc (see below).
  • TicketNote โ€” free-form notes appended to a ticket (AddTicketNote).

UUIDs are deterministic, not random: lib/rabbit/uid derives them as SHA1(namespace, seq) under a per-project namespace seeded from rabbit.holmes.haus + project name, so the same (project, sequence) pair always yields the same UUID.

Dependency validation (lib/rabbit/ticket/service.go)

validateDependsOn runs on both CreateTicket and UpdateTicket whenever depends_on is set:

  1. A ticket cannot depend on itself.
  2. Every dependency must exist and belong to the same project (store.dependencyProjects).
  3. On update only, store.hasPathTo walks the existing dependency graph to reject changes that would introduce a cycle (connect.CodeFailedPrecondition).

Sprint YAML and cross-repo routing

Sprints double as YAML files under sprints/ (sync.SprintFile): a sprint: block plus a features: list. Each FeatureItem carries its own repo, status, priority, assigned_to, depends_on, and acceptance_criteria. This is the literal mechanism by which one sprint plan fans out across repos โ€” e.g. sprints/2026-05-25-beaver-async-kafka-redesign.yml lists features tagged repo: beaver, repo: joel.holmes.haus, and the narwhal sites repo in the same sprint, each destined for a different codebase but tracked from one place.

rabbit sync --dir sprints [--all] reads the YAML and upserts sprints/tickets into the DB (lib/rabbit/sync/load.go); rabbit dump writes the DB back out to YAML (lib/rabbit/sync/dump.go). This lets sprint planning happen either as a hand-edited file or through the API/CLI, and keeps both in sync.

Cross-service integration (worker)

cmd/worker runs five Kafka consumer loops:

  • sprint / ticket / design consumers โ€” upsert incoming protobuf events into their respective Postgres tables and (for tickets) mirror the body to ticketsDir/<project-prefix>/tickets/<index>.md, plus publish a magpie Resource event so the ticket becomes searchable/indexable elsewhere.
  • narwhal doc consumer (lib/rabbit/narwhal/consumer.go) โ€” watches narwhal DesignDoc events; when a doc’s status becomes APPROVED, it extracts the ## Acceptance Criteria section from the doc body and calls DesignService.CreateForNarwhal to promote it into a Rabbit Design.
  • narwhal product consumer (lib/rabbit/narwhal/product_consumer.go) โ€” watches narwhal Product events and upserts them as Rabbit Project rows, keeping project metadata in sync with narwhal’s product catalog.

ConnectRPC API surface

Registered in cmd/api/main.go, one handler per service:

ServiceRPCs
ProjectServiceCreateProject, GetProject, ListProjects, UpdateProject
SprintServiceCreateSprint, GetSprint, ListSprints, UpdateSprint
TicketServiceCreateTicket, GetTicket, ListTickets, UpdateTicket, AssignTicket, SetTicketStatus, AddTicketNote
DesignServiceCreateDesign, GetDesign, ListDesigns, UpdateDesign, SpawnTickets

All services are built with connect.WithInterceptors() over an h2c handler so both gRPC-style and plain HTTP/JSON ConnectRPC clients work without TLS in local dev. CORS is wide open (AllowedOrigins: ["*"]) since this is a personal, non-public service.

Persistence

Postgres via pgxpool, migrations under lib/repo/migrations run automatically by cmd/api on startup using goose with an embedded filesystem (lib/repo/migrations.go). No ORM โ€” queries are hand-written SQL in each entity’s store.go.