The big picture
Every process, every store, and who talks to whom.
The stores
| Store | Holds | Authority |
|---|---|---|
| S3 bucket | Every repository's write-ahead log (repos/<id>/index, repos/<id>/entries/<ulid>.pack), release assets, LFS objects, attachments, avatars, Zoekt shards | The truth for git data |
| Postgres | Everything that is not git: users, sessions, keys, repositories, issues, pull requests, reviews, events, notifications, delivery queue, billing rows | The truth for everything else |
| Local disk | Bare repositories materialized from the log, one per repository, plus temp spools | A cache. Deletable any time |
The bucket and Postgres share no transaction. Whenever a change spans both, the code picks an order so that a crash between the two leaves something harmless: an orphaned log entry, a push that is not yet billed, an event that is lost rather than invented. You will see that reasoning, written as one-line comments, throughout the git code.
The processes
apps/api does almost everything. It serves the REST API under /api, the git smart-HTTP transport at the root (/<owner>/<repo>.git/...), an SSH listener, the GitHub-compatible GraphQL and REST APIs, and background work: the event outbox, pull-ref syncs, code indexing, attachment sweeps, import dispatch. It is pinned to one replica, because caches and purges are per process (ADR 0020).
apps/web renders pages. It holds no data of its own; it calls the API with the viewer's cookie.
apps/delivery is a small Go program that claims jobs from the delivery_job table and sends email and webhooks. It never reads domain tables (ADR 0031).
apps/importer clones a GitHub repository, pushes it to Ghost like any user would, and posts issues and pull requests back to the API in batches (ADR 0032).
apps/zoekt runs Zoekt's webserver. It keeps no state: it syncs the index shards the API published to the bucket.
apps/docs and apps/learn are static documentation sites.
Why one big API
Splitting the git transport, the REST API and the GitHub layer into services was considered and rejected each time for the same reason: each needs nearly every domain service (access checks, the commit point, issues, events). The pieces that were split out each have a property that justified it: delivery talks to hostile networks and must not share the API's event loop; imports run for minutes and must survive API deploys.