Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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