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