Skip to content
191 linesCodeBlameRaw
1# Contributing to g1t
2
3g1t is built the way it asks others to build: issues and pull requests on
4[g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), required checks that prove
5a change works, and a merge queue that keeps `main` passing.
6
7## A change ships with its docs
8
9Documentation is part of the product, held to the same bar as the code.
10A pull request that changes what someone can do, see or call changes the
11docs in the same pull request:
12
13| If you change | Update |
14| --- | --- |
15| Something a person does on g1t.sh | The guide for it in `apps/docs/src/content/docs/guides/` |
16| An API route, a field, or an MCP tool | `apps/api/src/operations.rs` descriptions (they feed the OpenAPI document and the API reference), the example and notes for it in `apps/api/src/reference.json`, `reference/mcp.md`, and any guide that shows the call. Then refresh the docs' copy of the OpenAPI document with `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi`; `cargo test` fails until you do. |
17| How agents behave | `guides/working-with-g1t.md`, and `apps/web/public/llms.txt` |
18| Settings, limits or prices | The page that names them, and the table it is in |
19| A new feature | A section in the guide that owns it, linked from the docs home if it is a new task |
20
21The style, in short: plain sentences, second person, no marketing words,
22sentence-case headings, a numbered list for steps, a table for options, and
23every API mention with its exact route or tool name. Examples are copyable
24and real. Nothing is documented that the code does not do.
25
26Check the docs build before you push:
27
28```sh
29cd apps/docs && npm run build
30```
31
32## Icons
33
34- **Interface icons come from [Lucide](https://lucide.dev/icons).** In the
35 apps, import them from `lucide-react`. In the docs, use `@lucide/astro`:
36 import an icon component, or give `apps/docs/src/components/Card.astro`
37 and `Aside.astro` a Lucide name such as `icon="git-branch"`. Starlight's
38 own `<Card>`, `<LinkCard>` and `<Aside>` draw Starlight's icon set, so the
39 docs import the wrappers in `apps/docs/src/components/` instead.
40- **Brand marks come from [Simple Icons](https://simpleicons.org)**
41 (`simple-icons`), and follow each brand's own usage guidelines. Add the
42 dependency with the first one you show.
43- **The g1t logo and illustrations are our own artwork**, in
44 `apps/web/app/components/logo.tsx` and `art.tsx`.
45- Never hand-copy an icon's SVG paths into a file. If Lucide lacks the
46 icon you need, pick the closest one that it has.
47
48## Interface components
49
50- **Build the site from the components in `apps/web/app/components/ui`**,
51 which are [shadcn/ui](https://ui.shadcn.com) components themed with
52 g1t's tokens: buttons, cards, badges, tables, tabs, separators,
53 breadcrumbs, menus, dialogs, sheets, tooltips, form controls.
54- **If shadcn has a component you need and `ui` doesn't, add it there**
55 first, themed with the tokens, then use it. Don't rebuild it inline.
56- **Change how something looks through the component's variants**, not
57 with a one-off class string at the call site. A long `className` on a
58 `div` that draws a card, a pill or a button is a component that should
59 exist.
60- Hover hints use the `Tooltip` component, never `title=`. Native
61 `<select>`, file inputs, checkboxes and radios use their `ui`
62 components.
63- **Every avatar is `Avatar`** (`ui/avatar.tsx`), and every agent's is
64 `Avatar` with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`),
65 which wraps it. That draws the agent's bot face (`AgentFace`,
66 `components/agent-face.tsx`) from its `look` or its `avatar_seed`, puts
67 the agent marker in its corner and names it "…, agent" to a screen
68 reader. Never hand-draw an agent's face, a sparkle, a letter on a tile or
69 the marker at a call site: a place that shows an agent differently is a
70 bug.
71- Look at every page you change, in light and dark, on a desktop and a
72 phone, before calling it done.
73
74## Before you push
75
76- `cargo test` in the crate or service you changed.
77- `npx tsc -b --force` in `apps/web` (the incremental build misses changes
78 in `packages/contracts`).
79- Look at what you changed in a browser. Screenshots catch what type
80 checks do not.
81
82## Deploying
83
84Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs
85`scripts/deploy.mjs`, which deploys only the parts that changed, migrations
86first. Every deployable part is listed in `deploy/stack.jsonc`; a new service
87or app goes there, and in the table on
88[How a self-hosted g1t runs](https://docs.g1t.sh/guides/self-hosting-architecture/#each-part)
89(`npm run test:deploy` says what is missing).
90[Deploy g1t to Cloudflare](https://docs.g1t.sh/guides/deploy-to-cloudflare/)
91covers the tool, the workflow, rollbacks, adding a unit and first-time
92setup.
93
94- **Migrations run before the code**, so the old code reads the new schema
95 for a minute or more. Add tables and columns; change what rows mean in
96 two deploys (code that reads both forms first); never drop what live code
97 still reads.
98- **The self-hosted runner** is released, not deployed: bump `version` in
99 `crates/runner/Cargo.toml`, merge, and push a `runner-v<version>` tag.
100 `.g1t/workflows/runner-release.yml` builds, signs and publishes it, and
101 runners update themselves to it. `scripts/runner-release.mjs` does the
102 same by hand.
103- **The desktop app** is released the same way: bump `version` in
104 `apps/desktop/src-tauri/Cargo.toml`, merge, and push a
105 `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds
106 the installers (Windows and Linux on Linux; macOS on a Mac runner when
107 one is registered), signs the updater's files and publishes them, and
108 installed apps update themselves. `scripts/desktop-release.mjs` does the
109 same by hand. The app loads the live site, so a change to the site needs
110 no release; only a change under `apps/desktop` does. The guide is
111 [The desktop app](https://docs.g1t.sh/guides/desktop/).
112
113## Speed
114
115Pages are a few rounds of service calls, and each round costs a trip to
116the databases, so start calls together and add no rounds.
117
118- Every page answers with `Server-Timing`: each loader, each service's
119 calls and their database time. Look at it in DevTools when a page feels
120 slow.
121- `scripts/perf/measure.ps1` times pages from your machine (`-BrowserUA`
122 for signed-out pages as a browser sees them). A page that only reads
123 must not set the `g1t_d1` cookie: a new read-only service method goes in
124 `READS` in `apps/web/app/lib/perf.ts`.
125- Targets from the US: signed-out pages under 200 ms to first byte,
126 signed-in pages under 400 ms, streamed panels within a second.
127- Workers run without Smart Placement; `scripts/perf/placement-probe.mjs`
128 measures a placement before you pin one.
129- A TypeScript service reports its database time too (`db;dur`, from
130 `openD1` in `@g1t/contracts`): the round trips a method makes are what
131 it costs, so start them together (`Promise.all`), keep what never
132 changes across requests in the isolate (the artifacts service keeps a
133 workspace's slug for 30 s and whether its General space exists), and
134 don't wake a Durable Object for a read D1 can answer.
135- Nothing live gates a page. A page carries what it shows (a doc's text
136 and its saved document), opens its sockets as it hydrates rather than
137 from inside the code they feed, keeps what is typed until they answer,
138 and says so with a dot, never a spinner. A page opened with an access
139 token gets its socket tickets with the page (the root loader mints the
140 feed's, an artifact's loader its room's) rather than asking for each.
141- Code that only the browser runs (the doc editor) is still fetched with
142 the page: `vite.config.ts` notes the chunk and what it imports for the
143 route's `links`, and a link to such a page warms the chunk on hover.
144 Modules several routes share are grouped by who shares them
145 (`codeSplitting.groups`), so a page fetches tens of files, not a
146 hundred and more. Before this, opening a doc fetched 124 files
147 (1,049 KB) in three waves and the editor appeared at 2.8–3.3 s; a doc
148 now fetches 71 files (1,026 KB) in one wave, and against the deployed
149 services the editor appears at 2.1 s, of which the room's answer is
150 0.65 s and the editor's own start 0.4 s. With the saved document in the
151 page, measured on g1t.sh signed in (2026-10-10, warm, 1440×900), the
152 editor appears at 1.5 s: first byte 0.53 s, scripts by 0.75 s, the
153 editor's own start the rest. A doc without a saved document (one last
154 saved before documents were kept) waited for its room's first message
155 instead, 0.9 s after the socket opened, and appeared at 2.0–2.5 s; the
156 page now asks the room once and keeps the answer, so only its first
157 open pays. Still to cut: the live socket's four service calls before
158 the room answers, the page and sidebar's 27 database round trips in two
159 calls, the root loader's billing and identity calls (0.7–1.3 s and
160 0.45 s inside), and the 3.4 MB of script a doc page decodes.
161
162## Rate limits
163
164`RATE_LIMITS` in `packages/contracts/src/rate-limits.ts` is the table of
165record, and the [rate limits page](https://docs.g1t.sh/reference/rate-limits/)
166is the public one: change both, and the binding in the Worker's
167`wrangler.jsonc`, together (`front-door-limits.test.ts` fails when they
168disagree). Limits fail open, keys hash anything secret, and each Worker
169takes its namespace ids from its own block of a hundred (41xx packages,
17042xx web, 43xx repos, 44xx api, 45xx og, 46xx status).
171
172## Operating g1t.sh
173
174For Flagon staff.
175
176- **Incidents** are declared, updated and resolved in sudo, under
177 **Platform → Incidents**, and appear on status.g1t.sh within 30 seconds.
178 Declare as soon as people are affected, and pick the higher severity
179 when unsure. SEV1 (down for most people, or data at risk) gets an update
180 at least every 30 minutes and SEV2 (a core part broken for many) at least
181 hourly; both email subscribers and need a blameless postmortem within
182 five working days.
183- **An Artifacts outage:** `scripts/ops/gitstore-namespaces.mjs` shows a
184 failing namespace. After 15 minutes, serve it read-only from the fallback
185 git store (`scripts/ops/restore-to-gitstore.mjs restore`, then the repos
186 Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To
187 switch back, `reconcile` anything the fallback took, then delete the
188 secret. The script's header has every step.
189- **Costs and margin**, model prices, credits and the platform pause are
190 in sudo, under **Costs & margin**. The code is in `services/billing/src`
191 (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`).