Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ jobs:
env:
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable

# Placeholder; finalized when apps/mobile-app lands.
# Typecheck, lint, Expo SDK version check and unit tests. The contract test
# (pnpm test:contract) needs Go + Docker and runs locally.
mobile:
needs: changes
if: ${{ !cancelled() && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.mobile == 'true')) }}
Expand All @@ -128,22 +129,22 @@ jobs:
steps:
- uses: actions/checkout@v7

# hashFiles is not available in job-level `if`, so each step checks it.
# Version from packageManager in package.json.
- uses: pnpm/action-setup@v6
with:
package_json_file: apps/mobile-app/package.json

- uses: actions/setup-node@v7
if: hashFiles('apps/mobile-app/package.json') != ''
with:
node-version: 22
cache: npm
cache-dependency-path: apps/mobile-app/package-lock.json

- if: hashFiles('apps/mobile-app/package.json') != ''
run: npm ci

- if: hashFiles('apps/mobile-app/package.json') != ''
run: npm run --if-present lint

- if: hashFiles('apps/mobile-app/package.json') != ''
run: npm run --if-present typecheck
cache: pnpm
cache-dependency-path: apps/mobile-app/pnpm-lock.yaml

- run: pnpm install --frozen-lockfile
- run: pnpm exec tsc --noEmit
- run: pnpm exec expo lint
- run: pnpm exec expo install --check
- run: pnpm test

ci-ok:
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copy to .env.local and adjust for your device.
#
# Origin of the Go server (apps/web, `make dev` binds 0.0.0.0:3000). Inlined
# at bundle time: restart `expo start` after changing it.
#
# iOS Simulator: http://localhost:3000
# Android Emulator: http://10.0.2.2:3000
# Physical device: your LAN IP or Tailscale hostname,
# e.g. http://my-mac.tailnet.ts.net:3000
#
# Never point this at a bare `localhost` when testing on a real device: the
# phone resolves localhost to itself, not to your dev machine.
EXPO_PUBLIC_API_URL=http://localhost:3000
46 changes: 46 additions & 0 deletions apps/mobile-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

example

# generated native folders
/ios
/android

# contract test build output and server log
.contract/
110 changes: 110 additions & 0 deletions apps/mobile-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# mobile-app

Expo (React Native) app for this template. It signs in against the Go
server's Better Auth–compatible endpoints (`packages/auth`, mounted at
`/api/auth/*`) and calls the JSON API (`/api/core/v1`) with its
`{ data, error }` envelope.

- **Framework**: Expo SDK 57, Expo Router (typed routes), React Compiler
- **Auth**: `better-auth` + `@better-auth/expo`, unchanged. The Go server keeps
Better Auth's paths, JSON shapes, cookie name and signing format, and trusts
the `expo-origin` header, so the stock Expo client works. The session cookie
lives in the device keychain (`expo-secure-store`).
- **API**: `src/lib/api/client.ts` attaches the session cookie to requests and
returns the envelope. Response types are in `src/lib/api/types.ts`; keep them
in sync with `apps/web/internal/api`.

This folder is a standalone pnpm project (its own `package.json` and
`pnpm-lock.yaml`). Nothing outside it needs Node.

The planned successor is a pair of native apps (Swift for iOS, Kotlin for
Android) against the same JSON API. This app stays the reference client until
then, so keep it working when the API changes.

## Run it

```bash
# repo root: the Go server
make db-up && make migrate && make dev # serves 0.0.0.0:3000

# this folder
pnpm install
cp .env.example .env.local # then set EXPO_PUBLIC_API_URL
pnpm start # press i (iOS) or a (Android)
```

`EXPO_PUBLIC_API_URL` is the Go server's origin as the device sees it:

| Client | URL |
| ---------------- | ------------------------------------------------------------------- |
| iOS Simulator | `http://localhost:3000` |
| Android Emulator | `http://10.0.2.2:3000` |
| Physical device | a host the phone can reach: LAN IP or Tailscale name, e.g. `http://my-mac.tailnet.ts.net:3000` |

A phone resolves `localhost` to itself, so a physical device needs a reachable
host, and the server must listen on all interfaces (the default `0.0.0.0`).
Env vars are inlined at bundle time: restart `expo start` after changing them.

In development, email verification is off, so sign-up signs you straight in.
In production it is on: sign-up sends a link and sign-in waits until it is
clicked.

## Checks

| | |
| --- | --- |
| `pnpm typecheck` | `tsc --noEmit` |
| `pnpm lint` | `expo lint` |
| `pnpm exec expo install --check` | dependency versions match the Expo SDK |
| `pnpm test` | unit tests (`src/**/*.test.ts`) |
| `pnpm test:contract` | the app's auth and API clients against a real Go server |

### Contract test

`pnpm test:contract` builds `apps/web/cmd/server` from this repo, starts a
throwaway Postgres 18 container on a free port, migrates, serves on a free
port in development mode, and runs `contract/app.contract.test.ts`: sign-up,
get-session, sign-out, wrong password, sign-in, `GET`/`PATCH /users`
(including validation and the 403 for another user), session restore on a
cold launch, and delete account. The app's real modules run in Node; only the
native modules are stubbed (`contract/stubs/`). Server output goes to
`.contract/server.log`.

It needs Go and Docker. Overrides:

| Variable | Default |
| --- | --- |
| `GO_SERVER_SRC` | `../..` (the repo root to build the server from) |
| `CONTRACT_SERVER_BIN` | build it (set to use a prebuilt `server` binary) |
| `CONTRACT_DATABASE_URL` | start a Postgres container |
| `GO` | `go` (e.g. `GO="$(mise which go)"`) |

## Structure

```
src/
app/ Expo Router routes ((auth) and (app) groups)
components/ Themed UI components
constants/ Theme colors, spacing
lib/
api/ apiFetch client, envelope helpers, API types
auth/ Better Auth client (SecureStore cookies)
config/ env.ts (EXPO_PUBLIC_API_URL) and routes.ts (AppRoutes/ApiRoutes)
services/ userService: get, update settings, delete account
validators/ Zod schemas, mirroring apps/web/internal/validate
contract/ contract test against the Go server
```

## Server side

`packages/auth` trusts the app's `betterstack://` scheme always and the Expo Go
/ dev-client origins (`exp://`) outside production
(`auth.MobileTrustedOrigins`). If you change `scheme` in `app.json`, change it
in `src/lib/auth/authClient.ts` and in `MobileTrustedOrigins` too.

## Notes

- Expo majors change quickly. Check the versioned docs for the SDK pinned in
`package.json` before upgrading: https://docs.expo.dev/versions/
- Web output is intentionally not configured; this template targets iOS and
Android with native auth flows.
38 changes: 38 additions & 0 deletions apps/mobile-app/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"expo": {
"name": "Better Stack Mobile",
"slug": "mobile-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "betterstack",
"userInterfaceStyle": "automatic",
"ios": {
"icon": "./assets/expo.icon"
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"backgroundColor": "#208AEF",
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
3 changes: 3 additions & 0 deletions apps/mobile-app/assets/expo.icon/Assets/expo-symbol 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile-app/assets/expo.icon/Assets/grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions apps/mobile-app/assets/expo.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"fill" : {
"automatic-gradient" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"image-name" : "expo-symbol 2.svg",
"name" : "expo-symbol 2",
"position" : {
"scale" : 1,
"translation-in-points" : [
1.1008400065293245e-05,
-16.046875
]
}
},
{
"image-name" : "grid.png",
"name" : "grid"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile-app/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile-app/assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile-app/assets/images/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading