Architecture
System Context
Container Diagram
Overview
Meerkat is a single-binary Go service (cmd/api) that exposes a Connect-RPC API over HTTP/2 (h2c) on port 9000, built on the shared archaea/server package for CORS, request logging, and graceful shutdown. It has two independent responsibilities that share nothing but a startup routine:
- Live GitHub status for a fixed, environment-configured list of repos.
- Durable project metadata for those (and any other) repos, stored in Postgres.
The projects Data Model
The projects table (migration lib/repo/migrations/00000001_projects.up.sql) has one row per repo:
| Column | Notes |
|---|---|
uuid | Primary key. UUID v5, deterministically derived from the slug (see below) โ never generated randomly. |
idx | Sequential integer from projects_idx_seq, assigned at insert time; used for stable list ordering. |
owner, repo, slug | slug ("owner/repo") is unique and is the natural key most callers use. |
name, description, url, local_path, prefix | Free-form metadata editable via UpdateProject. local_path is the local filesystem path other tooling writes exports to. |
status | active or archived, stored as text (lib/meerkat/projects/enums.go maps to/from the ProjectStatus proto enum). |
created_at, updated_at | Maintained by the DB; updated_at is bumped on every Update. |
UUIDs are computed by lib/project/uuid.go as uuid.NewSHA1(Namespace, []byte(slug)), where Namespace = uuid5(uuid.NamespaceURL, "meerkat.holmes.haus") is a fixed constant. Any service โ not just Meerkat โ can independently recompute the same UUID for a slug without calling the API, which is what lets other tools (like jh) reference a project by a stable ID they can derive locally.
The Two RPC Services
ProjectService (lib/meerkat/projects/service.go, backed by lib/meerkat/projects/store.go) is a conventional CRUD service over the projects table:
ListProjectsโ all projects, ordered byidx.GetProjectโ byuuidorslug(exactly one must be set).CreateProjectโ takes aslug, splits it intoowner/repo, derives the UUID, defaultsnameto the repo name if omitted, and rejects duplicate slugs withCodeAlreadyExists.UpdateProjectโ partial update via optional fields; every unset field is left unchanged (COALESCEin the SQL).
StatusService (lib/meerkat/grpc/service.go) is a thin read-only wrapper around the in-memory cache.Cache:
ListReposโ returns the cachedRepoStatusfor every watched repo.GetRepoโ returns the cached status for oneowner/repo, orNotFoundif it isn’t inWATCHED_REPOS.
It never talks to GitHub or Postgres directly โ all data comes from whatever the cache last fetched.
Live GitHub Status Refresh
On startup, main.go parses WATCHED_REPOS (a comma-separated list of owner/repo slugs) and constructs a cache.Cache (lib/cache/cache.go) over that fixed repo list. Cache.Start performs a synchronous first refresh โ the process blocks until every repo has been fetched at least once, so the API never serves an empty cache โ then launches a background goroutine that re-refreshes on a REFRESH_INTERVAL ticker (default 60s).
Each refresh fetches all watched repos in parallel via golang.org/x/sync/errgroup. Per repo, github.Client.FetchStatus (lib/github/client.go):
GET /repos/{owner}/{repo}for the default branch, thenGET .../commits?sha={branch}&per_page=1for the latest commit SHA (truncated to 7 chars).- In parallel,
GET .../actions/runs?branch={branch}&per_page=5(recent workflow runs) andGET .../pulls?state=open&per_page=20(open PRs).
A single repo’s fetch failure is logged and skipped (return nil from the errgroup func) rather than aborting the whole refresh cycle โ a bad GitHub response for one repo doesn’t blank out the rest of the dashboard. deriveCIStatus reduces the recent runs list to one of passing / failing / pending / unknown: any in_progress/queued run wins as pending; otherwise the first completed run’s conclusion (success โ passing, failure/timed_out โ failing) is used.
Seeding Projects from WATCHED_REPOS
After the cache’s first refresh, main.go calls seedProjects, which calls Store.SeedIfMissing for every watched slug. This does an INSERT ... ON CONFLICT (slug) DO NOTHING โ it guarantees a projects row exists for every watched repo (with a UUID, owner, repo, slug, name defaulted to the repo name, and status active), but never overwrites a row that already exists. That means metadata edited through UpdateProject โ description, URL, local path, prefix โ survives restarts even though the seed step runs every time the process boots.
Migrations
Schema migrations live under lib/repo/migrations/ as goose SQL files, embedded into the binary via embed.FS (lib/repo/migrations.go) and applied automatically at startup (goose.Up) against the pgx-backed *sql.DB โ no separate migration step or tool is required to deploy.