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.

279 linesCodeBlameRaw
1# g1t for Android and iOS
2
3The phone app: [Expo](https://expo.dev) (SDK 57, React Native 0.86) with
4Expo Router, in TypeScript. Work, Chat and Code, signed in with g1t's
5own OAuth. Android is the first target. What it does for people is in the
6guide, [The phone app](../docs/src/content/docs/guides/mobile.md).
7
8## Layout
9
10```text
11app.config.ts name, ids and icons per build variant (APP_VARIANT)
12eas.json EAS Build profiles: development, preview, production
13metro.config.js one React for the app in the monorepo
14scripts/icons.mjs draws the icons from packages/theme/mark.svg
15src/
16 app/ the screens (Expo Router: one file, one route)
17 sign-in.tsx Sign in with g1t, an access token, or another server
18 oauth/callback.tsx where the site sends sign-in back (g1t://oauth/callback)
19 (app)/ signed in: the tabs
20 work/ the workspace's work items: the list, an item, new
21 chat/ the list, a conversation, a thread, new, browse
22 code/ repositories, pull requests, diffs (opened from More)
23 more/ Code, notifications, workspaces, sign out
24 home/, notifications/ coming: they open the site
25 ref.tsx what #N names (a work item or a pull request), opened
26 +native-intent.tsx g1t:// and https links to screens (lib/links.ts)
27 components/ the building blocks, themed (ui.tsx); chat/, Code's and Work's
28 lib/
29 server.ts which g1t: site and API addresses
30 oauth.ts OAuth 2.1 with PKCE against the server's metadata
31 credentials.ts the server and credentials, in the phone's keystore
32 client.ts every request: refresh, errors, the site or the API
33 session.tsx who is signed in, to which workspace
34 chat.ts, code.ts, work.ts the data for Chat, Code and Work (TanStack Query)
35 live.ts the site's live sockets, with tickets, kept open
36 feed.tsx counts, banners and presence while the app is open
37 push.ts notifications on the phone
38 theme/ packages/theme/tokens.css's colours, for React Native
39```
40
41Shared types and catalogs come from `@g1t/contracts` (by subpath, such as
42`@g1t/contracts/oauth`, so the app does not bundle every module).
43
44## How it talks to g1t
45
46| What | Where | How it is signed in |
47| --- | --- | --- |
48| Who you are, your workspaces | `GET {api}/user` | `Authorization: Bearer g1t_…` |
49| Work | `{api}/workspaces/<workspace>/work…`, `…/labels`, `…/milestones` REST | the same |
50| Code | `{api}/repos…` REST | the same |
51| Chat | `{site}/<workspace>/-/chat/api` and its live socket, as the site's own pages use them | the same header, with `chat:*` ([below](#chat-with-the-apps-sign-in)); a socket gets a ticket from `{site}/-/live/ticket` first |
52| Notifications | `{site}/-/notify` and the feed socket `{site}/-/live` | the same, with `notifications:*` |
53| Sign-in | `{api}/.well-known/oauth-authorization-server`, `{site}/oauth/authorize`, `{api}/oauth/token` | OAuth 2.1, PKCE (S256), public client |
54
55The OAuth client is not registered anywhere: its id encodes its name and
56redirect (`encodeOAuthClient`), so `g1t for Android` with
57`g1t://oauth/callback` is the same client on every phone. Access tokens
58last 30 days and are refreshed a minute before they expire; a refresh
59token works once, so `lib/client.ts` shares one refresh between
60requests.
61
62### Chat with the app's sign-in
63
64Chat has no REST API: the app uses the site's own chat routes. The app
65asks for `chat:read`, `chat:write`, `notifications:read` and
66`notifications:write`, and the site takes a token holding them by name
67(OAuth or personal, without "Use the website as you") on these alone
68(`apps/web/app/lib/website-token.ts`, `scopedRoute` and `socketUser`):
69
70| Route | Needs |
71| --- | --- |
72| `GET`, `POST /<ws>/-/chat/api` | `chat:read`, `chat:write` |
73| `GET /<ws>/-/chat/person/<username>` | `chat:read` |
74| `GET /-/live/ticket` for `/<ws>/-/chat/live` and `/-/live` | `chat:read` |
75| `GET`, `POST /-/notify` | `notifications:read`, `notifications:write` |
76
77A full-access token does not count: it reaches Chat only with the website
78permission.
79
80### Live, and notifications
81
82- **A conversation's socket** (`lib/chat.ts`, `useChannelLive`) brings new,
83 changed and deleted messages, reactions and typing, and takes the
84 app's own typing.
85- **The feed** (`lib/feed.tsx`), the socket every tab of the site keeps,
86 is open while the app is in front: unread counts, notifications (a
87 banner, `components/chat/banner.tsx`) and presence. It tells g1t the app
88 is focused, so nothing is pushed to the phone while it is in use; in the
89 background it closes.
90- **Push** (`lib/push.ts`): the app registers its Expo push token with
91 `POST /-/notify` `{ intent: "register_device", device: { token, platform, name } }`
92 and takes it back on signing out (`unregister_device`). The notify
93 service sends through Expo's push service on the Android channel
94 `messages`, with `data` `{ href, workspace, kind, tag, channel_id, thread_root, card }`;
95 tapping one opens its thread or conversation.
96
97## Run it
98
99You need Node 24 (the repository's `.nvmrc`; EAS builds on 24.21.0, `eas.json`).
100
101### In Expo Go (quickest)
102
103Everything the app uses is in Expo Go for SDK 57, so no native build is
104needed:
105
106```sh
107npm install # at the repository's root: one workspace
108cd apps/mobile
109npx expo start --go # then scan the QR code with Expo Go
110```
111
112Install [Expo Go](https://expo.dev/go) on the phone, or press `a` for an
113Android emulator. The phone and the computer must be on the same network
114(or use `npx expo start --tunnel`).
115
116Signing in works in Go: the site sends you back to Go's own address,
117`exp://<your computer>:8081/--/oauth/callback`, which g1t accepts as an
118application's own scheme. The approval page and Connected applications
119call it **g1t in Expo Go**. Its client id encodes that address, so a new
120network makes a new client: the app keeps the id it signed in with for
121refreshing, and signing in again after a move just adds a new entry to
122Connected applications. Chat works in Go in full, live, with banners
123while the app is open. What Go can't do: notifications on an Android
124phone (Expo took push out of Go there), open from `g1t://` links (they go
125to an installed build), and show the app's own name and icon.
126
127To develop against a g1t running on your computer, choose **Server** on
128the sign-in screen and give its address on your network, such as
129`http://192.168.1.20:5173` and the API's `http://192.168.1.20:8789`
130(`localhost` on a phone is the phone). Run that g1t with its `SITE_URL`
131and `API_URL` set to those addresses too: the API's OAuth metadata
132names the sign-in page by `SITE_URL`, and the phone opens what it names.
133
134### In a development build
135
136For `g1t://` links, the app's own icon and name, and anything native Go
137lacks later (push notifications):
138
139```sh
140npm run build:android:dev # EAS builds g1t (dev) in the cloud: install it from the link
141npx expo start # then open g1t (dev) on the phone; it finds this computer
142```
143
144The development build is `g1t (dev)` (`sh.g1t.app.dev`), with
145`expo-dev-client`: it loads the app's code from `npx expo start` on your
146computer, signs in through `g1t://oauth/callback`, and gets notifications
147once Firebase is set up ([below](#push-notifications-on-android)). Build
148it again only when a native module or `app.config.ts` changes; code
149changes reload live. `.g1t/workflows/mobile-build.yml` builds it by hand
150(Run workflow, `development`); its default, `preview`, is what testers
151install, and main builds that again when the native side changes. The
152run's summary links the build on expo.dev, whose QR code installs it. With Android Studio, `APP_VARIANT=development npx expo
153run:android` builds the same app on your computer instead (it makes
154`android/`, ignored by git).
155
156Checks, which CI runs from the root (`npm run typecheck`, `npm test`):
157
158```sh
159npm run typecheck -w @g1t/mobile
160npm test -w @g1t/mobile # lib/*.test.ts under Node
161```
162
163## Set up EAS and ship Android
164
165Once, by someone with access to the Expo account and Google Play:
166
1671. `npm install -g eas-cli`, then `eas login`.
1682. The EAS project exists (`35fb457b-…`, `EAS_PROJECT` in
169 `app.config.ts`); `eas whoami` should be an account with access to it.
170 Set `EAS_OWNER` in your shell if it belongs to an organization. On
171 expo.dev, the project's GitHub settings take `apps/mobile` as the base
172 directory.
1733. `eas build --profile preview --platform android`. EAS makes and keeps
174 the upload keystore the first time; say yes. The build ends with a link
175 and a QR code to install the APK.
1764. In the [Google Play Console](https://play.google.com/console), create
177 the app with the package `sh.g1t.app`, and turn on Play App Signing.
1785. `npm run build:android` makes the production App Bundle. Upload the
179 first one by hand on Play's **Internal testing** track: Google requires
180 that before its API accepts uploads.
1816. For `eas submit`, make a Google Cloud service account with access to
182 the app in the Play Console, download its JSON key, and add it to the
183 EAS project's credentials (`eas credentials`). From then on,
184 `eas submit --platform android --latest` sends a build to internal
185 testing as a draft (`eas.json`'s `submit.production`).
186
187iOS is the same with `--platform ios`, an Apple Developer account and the
188bundle id `sh.g1t.app`; EAS makes the certificates.
189
190### Releasing on g1t.sh/download (until Google Play)
191
192Google Play and the App Store wait on Flagon's D-U-N-S number. Until then
193the `preview` build, `g1t`, is what people install, from
194g1t.sh/download:
195
196```sh
197# a new native side or version: bump `version` in app.config.ts, then
198git tag mobile-v0.1.0 && git push origin mobile-v0.1.0 # .g1t/workflows/mobile-release.yml
199# or by hand, from the repository's root:
200node scripts/mobile-release.mjs build # EAS builds the APK
201node scripts/mobile-release.mjs fetch # the newest finished preview build
202node scripts/mobile-release.mjs manifest
203node scripts/mobile-release.mjs publish # to g1t.sh/downloads/mobile/, latest.json last
204
205# code alone: over the air to every installed copy of this version
206cd apps/mobile && npx eas-cli update --channel preview --message "What changed"
207```
208
209The workflow needs `EXPO_TOKEN` (expo.dev → Access tokens) beside the
210deploy's Cloudflare secrets. Preview builds count their own versionCode
211(`autoIncrement`), so each installs over the last.
212
213### Push notifications on Android
214
215Expo's push service sends to Android through Firebase Cloud Messaging,
216which needs a Firebase project. Once:
217
2181. In the [Firebase console](https://console.firebase.google.com), make a
219 project, and in it an Android app for each package: `sh.g1t.app`,
220 `sh.g1t.app.preview` and `sh.g1t.app.dev`. Download
221 `google-services.json` (one file lists all three).
2222. Give it to EAS as a file variable:
223 `eas env:create --scope project --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json --visibility secret --environment production --environment preview --environment development`.
224 `app.config.ts` reads it from there, or a `google-services.json` beside
225 it locally (ignored by git).
2263. In Firebase, **Project settings → Service accounts → Generate new private
227 key**, then `eas credentials` → Android → the production build's package →
228 **Google Service Account → Push Notifications (FCM V1)**, and upload that
229 key. Repeat for `sh.g1t.app.preview` and `sh.g1t.app.dev`.
2304. Build again. Pushes need a build: Expo Go on Android has none.
231
232`EXPO_ACCESS_TOKEN` on the notify Worker is needed only if the Expo
233project turns on **Enhanced push security**. Leave that off: self-hosted
234g1ts push to this same app through this same project, without g1t's
235token, and turning it on stops their notifications.
236
237### Self-hosted g1t
238
239The app works with any g1t (docs: the phone app guide's "Your own g1t",
240and self-hosting's "The phone app"):
241
242- `GET <site>/.well-known/g1t.json` (`@g1t/contracts/discovery`,
243 `apps/web/app/routes/well-known-g1t.ts`) says where the API is and
244 whether the g1t sends phones notifications (`PHONE_PUSH`). The app reads
245 it in `lib/discover.ts`, and falls back to `api.<host>` for an older g1t.
246- `g1t://connect?server=<site>` (`app/connect.tsx`) points the app at a
247 g1t after asking; a self-hosted g1t's `/download` shows it as a QR code.
248- On g1t.sh none of it shows: the sign-in screen has one button, and a
249 quiet "Using your own g1t?" at its foot.
250- Store builds open `https` only (Android blocks cleartext); development
251 builds also open `http`, for a g1t on your network.
252
253### Before the first store release
254
255- **App Links**: `https://g1t.sh/.well-known/assetlinks.json` naming
256 `sh.g1t.app` and Play's signing certificate's SHA-256, then an
257 `intentFilters` entry with `autoVerify` in `app.config.ts`, so g1t.sh
258 links open the app. iOS needs `apple-app-site-association` the same way.
259- **The brand's faces**: TTF copies of Hanken Grotesk, Bricolage
260 Grotesque and IBM Plex Mono (packages/theme ships woff2), loaded with
261 `expo-font`.
262- **The store listing**: screenshots, a privacy policy URL, and Play's
263 data safety form (the app keeps the server and credentials in the
264 keystore, registers a push token with the g1t it is pointed at, and
265 sends nothing anywhere else).
266
267## Notes
268
269- **React is pinned.** The site uses a newer React than React Native 0.86
270 allows, so the app has its own `react` (19.2.3) and `metro.config.js`
271 resolves every `react` import to it. `expo-doctor` reports the root's
272 copy as a duplicate; that is expected here, and the bundle has one.
273 The doctor has no way to allow one duplicate, so EAS builds skip it
274 (`EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP` in `eas.json`); `npm run doctor`
275 still runs it.
276- **Icons** are drawn from the mark: `npm run icons -w @g1t/mobile`.
277- **Versions**: `version` in `app.config.ts` is what people see;
278 EAS counts build numbers itself (`appVersionSource: remote`). Over-the-air
279 updates go to builds of the same `version` (`runtimeVersion`).