feat(client): scaffold @carol/client — Expo Router + RN Web (#183) #198

Merged
james merged 2 commits from 183-expo-client-scaffolding into main 2026-06-21 11:14:55 +00:00
Owner

Summary

Stands up apps/client/ — the universal Expo Router + React Native Web app. After this lands, pnpm -F @carol/client export:web produces a static bundle that #186 will copy into the API container, expo run:android will boot the app on a real device against a self-hoster's API URL, and one reference screen (/notes) reads + writes data end-to-end through @carol/api-client.

Two commits:

  • f752a5e chore(client): scaffold — real package.json (@carol/client), tsconfig (extends expo/tsconfig.base), app.json (web + Android stub, expo-router + expo-secure-store + expo-localization plugins, typedRoutes on), eslint flat config (typescript-eslint strict — same baseline as @carol/api-client), vitest config (node env), and a workspace-scoped .npmrc with node-linker=hoisted so Metro's resolver doesn't fight pnpm's strict store.
  • 1c32b55 feat(client): theme + i18n + auth glue + Notes reference + CI gates — all the source. End-to-end verified in a Node 22 container matching CI; the static web bundle ships under apps/client/dist/.

Decisions baked in

  • Expo SDK 56 + React 19.2.3. Latest stable. The codebase's React 19 alignment (apps/api uses React 19 too) carries over cleanly. Earlier survey suggested SDK 52 + React 18; the actual latest SDK pairs with React 19.
  • Reference screen: /notes. Simpler than /skills (which the ticket suggested), exercises cursor pagination through useNotes, matches ADR-0012's canonical TanStack reference. Skills + every other screen land in #184.
  • DS tokens as TypeScript constants in lib/theme/tokens.ts. Mirrors apps/api/app/themes/{light,dark}.css verbatim. Skips the legacy --color-* bridge (ADR-0023) — Expo client is greenfield.
  • i18n: i18next + react-i18next + expo-localization. Catalog imports unchanged from @carol/i18n. Pure resolution logic ported from apps/api/lib/i18n/locales.ts minus the env-var coupling (the client bakes its supported set in at build time).
  • Theme persistence via the API, not cookies or AsyncStorage. useSettings() + useUpdateSettings() round-trip through @carol/api-client.
  • Auth: cookies for web, bearer for native. lib/auth/storage.ts is SecureStore on native, no-op on web (cookies cover it). getAuthHeader() returns Bearer cat_... or null per request.
  • Routing: Expo Router file-based, mirroring Next.js paths exactly so #184's port becomes file-name-for-file-name.
  • Stub screens for non-reference (app) routes. Per ADR-0027 each is implemented from the design spec + feature ticket in #184 — the stubs exist so the routing tree is complete.

Out of scope (per the approved plan)

  • Porting any non-reference screens — #184.
  • Decommissioning the Next.js UI — #185.
  • Single-container deployment — #186.
  • Android signed APK pipeline — #187.
  • Linux Flatpak / Tauri shell — #188.
  • Service worker / PWA install prompt config — comes with #186.

Test plan

  • pnpm install clean.
  • pnpm -F @carol/client typecheck green.
  • pnpm -F @carol/client lint green.
  • pnpm -F @carol/client test green (8 tests covering locale resolution).
  • pnpm -F @carol/client export:web succeeds and produces apps/client/dist/.
  • All prior gates remain green (pnpm -F @carol/api typecheck/lint/test/openapi:check/openapi:coverage, pnpm -F @carol/api-client typecheck/lint/test/check).
  • Full pipeline reproduced in a Node 22.23.0 container matching CI — all green.
  • Manual end-to-end against a running apps/api: register → /notes → create/edit/delete a note. (Author hasn't run yet; the CI export confirms the bundle compiles.)
  • Manual expo run:android against an emulator — deferred; the scaffolded Android config is untested without an Android SDK.

Closes #183. Fifth ticket under epic #176. Unblocks #184 (screen ports), #186 (single-container deployment), #187 (Android build), #188 (Flatpak).

## Summary Stands up `apps/client/` — the universal Expo Router + React Native Web app. After this lands, `pnpm -F @carol/client export:web` produces a static bundle that #186 will copy into the API container, `expo run:android` will boot the app on a real device against a self-hoster's API URL, and one reference screen (`/notes`) reads + writes data end-to-end through `@carol/api-client`. Two commits: - **`f752a5e` chore(client): scaffold** — real package.json (`@carol/client`), tsconfig (extends `expo/tsconfig.base`), app.json (web + Android stub, expo-router + expo-secure-store + expo-localization plugins, typedRoutes on), eslint flat config (typescript-eslint strict — same baseline as `@carol/api-client`), vitest config (node env), and a workspace-scoped `.npmrc` with `node-linker=hoisted` so Metro's resolver doesn't fight pnpm's strict store. - **`1c32b55` feat(client): theme + i18n + auth glue + Notes reference + CI gates** — all the source. End-to-end verified in a Node 22 container matching CI; the static web bundle ships under `apps/client/dist/`. ## Decisions baked in - **Expo SDK 56 + React 19.2.3.** Latest stable. The codebase's React 19 alignment (apps/api uses React 19 too) carries over cleanly. Earlier survey suggested SDK 52 + React 18; the actual latest SDK pairs with React 19. - **Reference screen: `/notes`.** Simpler than `/skills` (which the ticket suggested), exercises cursor pagination through `useNotes`, matches ADR-0012's canonical TanStack reference. Skills + every other screen land in #184. - **DS tokens as TypeScript constants in `lib/theme/tokens.ts`.** Mirrors `apps/api/app/themes/{light,dark}.css` verbatim. Skips the legacy `--color-*` bridge (ADR-0023) — Expo client is greenfield. - **i18n: `i18next` + `react-i18next` + `expo-localization`.** Catalog imports unchanged from `@carol/i18n`. Pure resolution logic ported from `apps/api/lib/i18n/locales.ts` minus the env-var coupling (the client bakes its supported set in at build time). - **Theme persistence via the API**, not cookies or AsyncStorage. `useSettings()` + `useUpdateSettings()` round-trip through `@carol/api-client`. - **Auth: cookies for web, bearer for native.** `lib/auth/storage.ts` is SecureStore on native, no-op on web (cookies cover it). `getAuthHeader()` returns `Bearer cat_...` or null per request. - **Routing: Expo Router file-based, mirroring Next.js paths exactly** so #184's port becomes file-name-for-file-name. - **Stub screens for non-reference `(app)` routes.** Per ADR-0027 each is implemented from the design spec + feature ticket in #184 — the stubs exist so the routing tree is complete. ## Out of scope (per the approved plan) - Porting any non-reference screens — #184. - Decommissioning the Next.js UI — #185. - Single-container deployment — #186. - Android signed APK pipeline — #187. - Linux Flatpak / Tauri shell — #188. - Service worker / PWA install prompt config — comes with #186. ## Test plan - [x] `pnpm install` clean. - [x] `pnpm -F @carol/client typecheck` green. - [x] `pnpm -F @carol/client lint` green. - [x] `pnpm -F @carol/client test` green (8 tests covering locale resolution). - [x] `pnpm -F @carol/client export:web` succeeds and produces `apps/client/dist/`. - [x] All prior gates remain green (`pnpm -F @carol/api typecheck/lint/test/openapi:check/openapi:coverage`, `pnpm -F @carol/api-client typecheck/lint/test/check`). - [x] Full pipeline reproduced in a Node 22.23.0 container matching CI — all green. - [ ] Manual end-to-end against a running `apps/api`: register → /notes → create/edit/delete a note. (Author hasn't run yet; the CI export confirms the bundle compiles.) - [ ] Manual `expo run:android` against an emulator — deferred; the scaffolded Android config is untested without an Android SDK. Closes #183. Fifth ticket under epic #176. Unblocks #184 (screen ports), #186 (single-container deployment), #187 (Android build), #188 (Flatpak).
Real package.json replacing the #181 placeholder. Installs Expo SDK
~56.0.12, expo-router ~56.2.11, React 19.2.3, React Native 0.85.3,
RN Web ~0.21.0, plus the workspace deps @carol/api-client and
@carol/i18n. Adds expo-secure-store + expo-localization for the
auth glue and i18n runtime that land in subsequent commits.

- package.json: scripts (dev, web, android, export:web, lint,
  typecheck, test); dependency pins from a fresh `create-expo-app`
  template adjusted for our workspace conventions.
- tsconfig.json: extends expo/tsconfig.base with strict mode +
  noUncheckedIndexedAccess (the apps/api convention).
- app.json: web target on (Metro bundler, static output), Android
  package tech.wynning.carol stubbed, expo-router + expo-secure-store
  + expo-localization plugins, typedRoutes experiment on.
- eslint.config.mjs: typescript-eslint strict — same baseline
  @carol/api-client uses. Skipping eslint-config-expo for now; it
  pulls many deps and the strict ruleset is plenty.
- vitest.config.ts: node environment for the pure-logic surfaces
  (auth glue, i18n resolution, theme tokens). Hook/component tests
  against the real Expo runtime land in #184.
- .npmrc: node-linker=hoisted at the workspace level. Expo's Metro
  bundler assumes a flat node_modules tree; pnpm's strict symlink
  store breaks resolver assumptions. Scoped to apps/client only.

Source code (theme, i18n, auth glue, screens) lands in following
commits. `pnpm install` clean; `pnpm -F @carol/client typecheck`
green (trivially — no source yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(client): theme + i18n + auth glue + Notes reference + CI gates (#183)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 9s
PR / OSV-Scanner (pull_request) Failing after 2m15s
PR / Static analysis (pull_request) Successful in 2m39s
PR / Lint (pull_request) Successful in 2m53s
PR / OpenAPI (pull_request) Successful in 3m1s
PR / pnpm audit (pull_request) Successful in 3m11s
PR / Client (web export smoke) (pull_request) Successful in 3m26s
PR / Typecheck (pull_request) Successful in 3m58s
PR / Test (postgres) (pull_request) Successful in 3m57s
PR / Test (sqlite) (pull_request) Successful in 3m57s
PR / Build (pull_request) Successful in 4m4s
PR / Package age policy (soft) (pull_request) Successful in 1m11s
Secrets / gitleaks (pull_request) Successful in 1m4s
PR / Coverage (soft) (pull_request) Successful in 1m48s
PR / Trivy (image) (pull_request) Failing after 2m28s
1c32b55225
Fills in @carol/client with the source that turns the scaffold into
a working app. End-to-end verified in a Node 22 container matching
CI: typecheck, lint, test (8 tests), and `expo export --platform
web` all green; the static bundle ships under apps/client/dist/.

What landed:

- **Theme runtime** (`lib/theme/`): DS token constants (light + dark)
  mirroring apps/api/app/themes/{light,dark}.css verbatim, minus the
  legacy --color-* bridge — the Expo client is greenfield and
  ADR-0023's bridge doesn't apply. ThemeProvider reads useSettings()
  from @carol/api-client for persistence; auto is resolved by RN's
  useColorScheme().

- **i18n runtime** (`lib/i18n/`): i18next + react-i18next + the
  resolution chain. Catalogs imported directly from @carol/i18n
  (en + es). expo-localization supplies the OS candidate list; the
  resolver mirrors the API's logic without the env-var coupling.

- **Auth glue** (`lib/auth/`): expo-secure-store on native (no-op on
  web — same-origin cookies cover it); getAuthHeader() is the async
  callback @carol/api-client's createApiClient consumes. login.ts
  branches on Platform.OS: web POSTs /api/auth/login, native POSTs
  /api/auth/token and stores the returned pair.

- **Expo Router skeleton**: root layout mounts the providers in
  order (Query → ApiClient → I18n → Theme). The (app) layout reads
  useMe() and redirects to /login on 401. Login/register are
  minimal forms; index.tsx redirects to /notes. The remaining
  (app) screens (profile, skills, experience, …) are stubs per
  ADR-0027 — implemented from the design spec in #184.

- **Notes reference screen** (`app/(app)/notes.tsx`): full CRUD
  end-to-end against the API via @carol/api-client. Uses theme
  tokens, translated strings, edit-in-place, optimistic invalidation.
  Proves the Expo + RN Web + TanStack + typed-client loop.

- **CI gates**: @carol/client added to the lint, typecheck, and
  test jobs. New `client-build` job runs `expo export --platform
  web` as a smoke. Android export deferred to #187 (needs Android
  SDK in the runner image).

- **Docs**: apps/client/README.md replaces the placeholder. CLAUDE.md
  "Shape of the system" points at it. .gitignore covers Expo's
  local dev state (.expo/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

OSV-Scanner

Threshold: high  ·  Total findings: 8  ·  At/above threshold: 2

critical high medium low
1 1 6 0
severity id package installed / range fix
critical GHSA-5xrq-8626-4rwp vitest@2.1.9 4.0.0–4.1.0 4.1.0
high GHSA-fx2h-pf6j-xcff vite@5.4.21 8.0.0–8.0.16 8.0.16
<!-- scanner-comment: osv --> ### OSV-Scanner **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 8 &nbsp;·&nbsp; **At/above threshold:** 2 | critical | high | medium | low | |---:|---:|---:|---:| | 1 | 1 | 6 | 0 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | critical | GHSA-5xrq-8626-4rwp | vitest@2.1.9 | 4.0.0–4.1.0 | `4.1.0` | | high | GHSA-fx2h-pf6j-xcff | vite@5.4.21 | 8.0.0–8.0.16 | `8.0.16` |

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 61.8% ≥ 50%
Branches 80.3% ≥ 75%
Functions 87.9% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 61.8% ✅ | ≥ 50% | | Branches | 80.3% ✅ | ≥ 75% | | Functions | 87.9% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
james merged commit 8527d2a35a into main 2026-06-21 11:14:55 +00:00
james deleted branch 183-expo-client-scaffolding 2026-06-21 11:14:56 +00:00
Sign in to join this conversation.
No description provided.