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 anet/httpmux, backed by Postgres (pgxpool) and Kafka (viagithub.com/holmes89/archaea/kafka). Runsgoosemigrations (lib/repo/migrations) on boot, then servesProjectService,SprintService,TicketService, andDesignService. Also exposesGET /healthandGET /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_seqcounters (next_ticket_seq,next_design_seqin the DB), optionalmeerkat_project_uuidandproduct_uuidslinking 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 aSprintStatus: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, plusBLOCKED, plusDRAFTfor proposals awaiting review โ see below), andPriority(LOW/MEDIUM/HIGH).reponames 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_onis a list of other ticket UUIDs;blocked_by_dependencyis computed on read (true if any dependency isn’tDONE) and is advisory only โ it never blocks a status transition. A ticket created withCreateTicketRequest.draft = truestarts inDRAFTinstead ofTODO;DRAFTtickets are omitted fromListTicketsunlessstatusis set toDRAFTexplicitly, and are “approved” by moving them toTODO(SetTicketStatus, orrabbit ticket approve <uuid>). - Design โ a spec: title/body/description plus a list of
acceptance_criteria.SpawnTicketsturns each acceptance-criteria line into a realTASKticket linked back to the design viadesign_uuid. Designs can originate from an approved narwhalDesignDoc(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:
- A ticket cannot depend on itself.
- Every dependency must exist and belong to the same project
(
store.dependencyProjects). - On update only,
store.hasPathTowalks 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/designconsumers โ upsert incoming protobuf events into their respective Postgres tables and (for tickets) mirror the body toticketsDir/<project-prefix>/tickets/<index>.md, plus publish amagpieResourceevent so the ticket becomes searchable/indexable elsewhere.narwhaldoc consumer (lib/rabbit/narwhal/consumer.go) โ watches narwhalDesignDocevents; when a doc’s status becomesAPPROVED, it extracts the## Acceptance Criteriasection from the doc body and callsDesignService.CreateForNarwhalto promote it into a RabbitDesign.narwhalproduct consumer (lib/rabbit/narwhal/product_consumer.go) โ watches narwhalProductevents and upserts them as RabbitProjectrows, keeping project metadata in sync with narwhal’s product catalog.
ConnectRPC API surface
Registered in cmd/api/main.go, one handler per service:
| Service | RPCs |
|---|---|
ProjectService | CreateProject, GetProject, ListProjects, UpdateProject |
SprintService | CreateSprint, GetSprint, ListSprints, UpdateSprint |
TicketService | CreateTicket, GetTicket, ListTickets, UpdateTicket, AssignTicket, SetTicketStatus, AddTicketNote |
DesignService | CreateDesign, 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.