| 1 | --- |
| 2 | title: Events |
| 3 | description: How a state change in one service reaches every other that cares, through one log and a queue per subscriber, and how webhooks, live pages and followers read it. |
| 4 | --- |
| 5 | |
| 6 | import Aside from '../../../components/Aside.astro'; |
| 7 | import Diagram from '../../../components/Diagram.astro'; |
| 8 | |
| 9 | Every state change in g1t is published as an event: a push, an issue |
| 10 | opened, a pull request merged, a workflow finished, a workspace renamed. |
| 11 | Services do not call each other to announce what happened. They publish, |
| 12 | and the `events` service (`services/events`, Rust) delivers. |
| 13 | |
| 14 | ## The envelope |
| 15 | |
| 16 | Every event has the same shape, which follows CloudEvents |
| 17 | (`crates/contracts/src/events.rs`, `packages/contracts/src/events.ts`): |
| 18 | |
| 19 | | Field | What it is | |
| 20 | | --- | --- | |
| 21 | | `id` | `evt_` and a UUIDv7: sortable by the time it was made | |
| 22 | | `type` | Such as `git.push`, `pull.merged`, `workflow.completed` | |
| 23 | | `source` | The service that published it | |
| 24 | | `time` | When, in RFC 3339 | |
| 25 | | `repoId` | The repository it is about, or none | |
| 26 | | `actor` | The account that caused it, or none | |
| 27 | | `data` | The type's own fields, in its contract | |
| 28 | |
| 29 | Coordination has its own events, published by `work`: sessions starting |
| 30 | and ending, claims, hand-offs, dependencies, overlaps, and the merge |
| 31 | queue's verdicts (`integration.failed`, `integration.passed`, |
| 32 | `decision.recorded`). They reach webhooks and followers like any other |
| 33 | ([Work and the merge queue](/architecture/work/#coordination)). |
| 34 | |
| 35 | Two fields in `data` matter everywhere: |
| 36 | |
| 37 | - `causedByJob` marks an event caused by a workflow's own token, so it |
| 38 | does not start more workflows. |
| 39 | - `truncated: true` marks an event that was too big for a queue message |
| 40 | (96 KB) and had its long strings cut, step by step, until it fit. |
| 41 | |
| 42 | ## From publish to every subscriber |
| 43 | |
| 44 | <Diagram name="event-fanout" /> |
| 45 | |
| 46 | 1. A service calls `events`' `publish` with a batch of events. `events` |
| 47 | gives each its id and time and puts them on the `g1t-events` queue, then |
| 48 | returns. Publishing never waits on a subscriber. |
| 49 | 2. `events` reads its own queue, 100 messages at a time, and for each |
| 50 | batch: |
| 51 | - writes the events to the log with `INSERT OR IGNORE`, so a batch that |
| 52 | comes round twice is written once; |
| 53 | - sends each subscriber queue the events routed to it; |
| 54 | - tells `notify`, so open pages of that repository refresh, and puts |
| 55 | items in people's inboxes, at most once per person per event. |
| 56 | 3. If a send to a subscriber fails, `events` notes which queues already |
| 57 | have the batch (`fanout_sent`, kept a day) and throws. The retry skips |
| 58 | those queues. |
| 59 | 4. After three tries, a batch goes to `g1t-events-dlq`. |
| 60 | |
| 61 | Every subscriber's consumer is set up the same way: three tries, then the |
| 62 | dead-letter queue. |
| 63 | |
| 64 | ### Subscriber queues |
| 65 | |
| 66 | Each service that reacts to events has its own queue, |
| 67 | `g1t-events-<service>`. It gets only the types it needs, as `ROUTES` in |
| 68 | `crates/contracts/src/subscribers.rs` says, plus every lifecycle event |
| 69 | (renames, transfers and deletions of repositories, workspaces and users), |
| 70 | which every subscriber gets. |
| 71 | |
| 72 | | Queue | Read by | Gets | |
| 73 | | --- | --- | --- | |
| 74 | | `g1t-events-work` | `work` | `git.push`, `branch.renamed`, `repo.created`, `repo.archived`, `repo.default_branch_changed`, `pull.merged`, `comment.created` | |
| 75 | | `g1t-events-runner` | `runner` | Pull request changes, `checks.completed`, `queue.changed`, `comment.created`, issue changes, `repo.archived` | |
| 76 | | `g1t-events-actions` | `actions` | `git.push`, `git.deleted`, `pull.*`, `issue.*`, `comment.*`, `release.*`, deployments, `workflow.completed`, `repo.archived` | |
| 77 | | `g1t-events-webhooks` | `webhooks` | Everything | |
| 78 | | `g1t-events-security` | `security` | `git.push`, pull request changes, `comment.created`, `repo.created`, `repo.visibility_changed` | |
| 79 | | `g1t-events-search` | `search` | `git.push`, `repo.*`, `issue.*`, `pull.*`, `user.*`, `workspace.*` | |
| 80 | | `g1t-events-deployments` | `deployments` | Pushes, the pull request lifecycle, workspace deletion and restore | |
| 81 | | `g1t-events-projects` | `projects` | Repositories, packages and deployments | |
| 82 | | `g1t-events-repos` | `repos` | `pull.merged`, `pull.closed`, `pull.reopened`, deletions and restores | |
| 83 | | `g1t-events-packages` | `packages` | `git.push`, `repo.visibility_changed`, workspace and team changes | |
| 84 | | `g1t-events-integrations` | `integrations` | `issue.closed`, `pull.opened` | |
| 85 | | `g1t-events-billing` | `billing` | Lifecycle events only | |
| 86 | | `g1t-events-docs` | `artifacts` | `git.push`, `pull.merged` | |
| 87 | |
| 88 | A subscriber should treat every event as possibly delivered twice, and |
| 89 | make its handling idempotent: most do it with a unique key on the event's |
| 90 | `id`. |
| 91 | |
| 92 | ## The log |
| 93 | |
| 94 | `g1t-events`' D1 database keeps the log. Each event gets a `seq` as it is |
| 95 | written, one higher than the last; the log has one writer, the consumer |
| 96 | above. Ids are made by publishers in many places, so they can arrive out |
| 97 | of order. Reading "everything after this event" therefore follows `seq`, |
| 98 | not `id`. |
| 99 | |
| 100 | You can read a repository's log with the API's `list_events`, newest |
| 101 | first, 50 a page and up to 200, filtered by type. Status and check events |
| 102 | are left out unless you ask for their type. The same database keeps the |
| 103 | audit log, for 400 days. |
| 104 | |
| 105 | ### Following |
| 106 | |
| 107 | Pass `after`, an event id, and `list_events` returns what the log wrote |
| 108 | after it, oldest first. Add `wait`, up to 25 seconds, and the call waits |
| 109 | for something to arrive: |
| 110 | |
| 111 | <Diagram name="follow-events" /> |
| 112 | |
| 113 | The API asks the log about once a second while it waits (`apps/api/src/follow.rs`). |
| 114 | `g1t work events --follow` runs this in a loop, starting each call from the |
| 115 | last event it saw, so it misses nothing between calls. Coding agents get |
| 116 | the same over MCP, with the `list_events` tool. |
| 117 | |
| 118 | ## Webhooks |
| 119 | |
| 120 | `webhooks` reads every event and delivers those each subscription asks |
| 121 | for: |
| 122 | |
| 123 | | | | |
| 124 | | --- | --- | |
| 125 | | **Request** | `POST` with JSON, to public addresses only, with a 10 second timeout. Anything but a 2xx is a failure. | |
| 126 | | **Headers** | `x-g1t-event`, `x-g1t-delivery`, `x-g1t-hook`, and `x-g1t-signature-256: sha256=<HMAC-SHA256 of the body>` | |
| 127 | | **Body** | `{ id, type, time, workspace, repository: { id, full_name }, actor: { id, username }, data }`, in snake_case | |
| 128 | | **Retries** | After 1 minute, 5 minutes, 30 minutes, 2 hours and 5 hours: six attempts in about seven and a half hours | |
| 129 | | **At most once** | A delivery is unique on the subscription and the event's `id` | |
| 130 | | **Kept** | 14 days | |
| 131 | |
| 132 | Each subscription's secret is sealed in D1. [Webhooks](/guides/webhooks/) |
| 133 | shows how to check a signature. |
| 134 | |
| 135 | ## Live pages |
| 136 | |
| 137 | `notify` (TypeScript) keeps three kinds of Durable Object: |
| 138 | |
| 139 | | Object | One per | Holds | |
| 140 | | --- | --- | --- | |
| 141 | | `Feed` | Person | Their open tabs' sockets, notifications, counts and push subscriptions | |
| 142 | | `Room` | Workspace | Who is here | |
| 143 | | `Topic` | Repository | The pages open on it, told by `repo_changed` to refresh | |
| 144 | |
| 145 | Sockets use the WebSocket Hibernation API, so an idle tab costs nothing |
| 146 | while it waits. Browser push uses VAPID, and phones get Expo push; both are |
| 147 | sent to live 12 hours. |
| 148 | |
| 149 | `chat` and `artifacts` work the same way with rooms of their own: a |
| 150 | `ChannelRoom` per channel, and a `PageRoom` per doc that syncs edits as a |
| 151 | Yjs document and saves them to D1 a few seconds after a burst of typing. |
| 152 | |
| 153 | <Aside title="Scheduled jobs"> |
| 154 | Some work runs on a clock rather than on events: webhook retries every |
| 155 | minute, Actions schedules every minute, fork retirement hourly, backups |
| 156 | nightly, the event log's purge daily. Each service's `wrangler.jsonc` |
| 157 | lists its `crons`. |
| 158 | </Aside> |