[Epic] Split frontend/backend — universal Expo client + OpenAPI-contracted API #176

Closed
opened 2026-06-20 15:43:21 +00:00 by james · 0 comments
Owner

Carol's frontend and backend become totally separate from now on. The Next.js app stops rendering UI and becomes a pure API server. A single universal client built on Expo Router + React Native Web takes over the PWA, the Android app, and the Linux Flatpak build. The contract between the two is an OpenAPI 3.1 spec — generated from the API's zod schemas, served at /api/openapi.json, and the only sanctioned way for the client to learn about endpoints, shapes, and errors.

This supersedes #6 (the React Native native-app epic) and reshapes the back half of #3 (PWA shell): the PWA becomes the Expo Web build, not the Next.js render.

Decisions baked in

  • Universal client — Expo Router + React Native Web. One codebase, three targets: PWA (web), Android, Linux Flatpak (the web build wrapped in a Flatpak shell — RN → Flatpak directly is not a real path).
  • OpenAPI is the contract. Source of truth is the zod schemas the API routes already use; the spec is generated, never hand-authored. CI fails if any route is missing from the spec or drifts from its zod schema.
  • No source-level code sharing across the boundary. The client consumes the API exclusively through a generated typed client + TanStack Query hooks, packaged as a workspace package. No import from apps/api in apps/client — ever.
  • No server components, no server actions, no RSC. Every data fetch is an HTTP call from the client. Everything is behind login except /login and /register, so SSR earns nothing.
  • Auth supports both cookies (web, same-origin) and bearer tokens (native). ADR-0015's OAuth2 flow extends with refresh-token rotation for native clients.
  • Single-container deployment stays. The API container also serves the Expo Web static bundle. Self-hoster UX is unchanged: one image, one env file.
  • TanStack Query / Form / Table carry over — headless cores work in RN; only the leaf DOM components get rewritten.

Linked tickets

  • ADR: universal Expo client + OpenAPI contract (the decision document)
  • OpenAPI spec generation from zod + /api/openapi.json + CI drift gate
  • API contract hardening (zod everywhere, error envelope, pagination, versioning)
  • Token-based auth: bearer + refresh flow for native; cookie path stays for same-origin web
  • Repo restructure into workspaces (apps/api, apps/client, packages/api-client)
  • Generated typed API client + TanStack Query hooks package
  • Expo client scaffolding (Expo Router, RN Web, theming port, i18n, offline)
  • Port existing screens to Expo Router (Profile, Skills, Experience, Settings, Notes, Login, Register, Home, Offline)
  • Decommission Next.js UI (app/(app)/*, app/login, app/register, app/page.tsx)
  • Single-container deployment: API serves the Expo Web bundle as a static fallback
  • Android build (EAS Build or self-hosted)
  • Linux Flatpak build (Expo Web bundle in a Flatpak shell)

Cross-epic dependencies

  • Supersedes #6 (React Native native-app epic). Close #6 when this lands.
  • Reshapes #3 (PWA shell): the PWA becomes the Expo Web build. Theme tokens (ADR-0023) port over to Expo's runtime — the biggest line item inside the Expo scaffolding ticket.
  • ADR-0025 (next-intl) gets superseded by an Expo-compatible i18n setup; the messages/*.json catalog stays, the runtime swaps.
  • ADR-0004 (server-side sessions) and ADR-0015 (OAuth2) get extended for bearer-token issuance.
  • ADR-0012 (TanStack) is reaffirmed.

Exit criteria

  • The Next.js app, when run, serves only API routes and the static Expo Web bundle. No server-rendered UI.
  • GET /api/openapi.json returns a valid OpenAPI 3.1 document covering every authenticated route. CI fails if a route is missing or the spec disagrees with its zod schema.
  • The Expo client, built once, runs as the installable PWA and as an Android APK pointed at a self-hoster's URL and as a Linux Flatpak — all hitting the same generated typed client.
  • A second user on the same instance still sees none of the first user's data; the cross-user 404 contract continues to hold at the API layer.
  • CLAUDE.md "Shape of the system" and idea.md are updated; the old "PWA = same Next.js app" wording is gone.
Carol's frontend and backend become **totally separate** from now on. The Next.js app stops rendering UI and becomes a pure API server. A single universal client built on **Expo Router + React Native Web** takes over the PWA, the Android app, and the Linux Flatpak build. The contract between the two is an **OpenAPI 3.1 spec — generated from the API's zod schemas, served at `/api/openapi.json`, and the only sanctioned way for the client to learn about endpoints, shapes, and errors**. This **supersedes #6** (the React Native native-app epic) and reshapes the back half of **#3** (PWA shell): the PWA becomes the Expo Web build, not the Next.js render. ## Decisions baked in - **Universal client** — Expo Router + React Native Web. One codebase, three targets: PWA (web), Android, Linux Flatpak (the web build wrapped in a Flatpak shell — RN → Flatpak directly is not a real path). - **OpenAPI is the contract.** Source of truth is the zod schemas the API routes already use; the spec is *generated*, never hand-authored. CI fails if any route is missing from the spec or drifts from its zod schema. - **No source-level code sharing across the boundary.** The client consumes the API exclusively through a generated typed client + TanStack Query hooks, packaged as a workspace package. No `import` from `apps/api` in `apps/client` — ever. - **No server components, no server actions, no RSC.** Every data fetch is an HTTP call from the client. Everything is behind login except `/login` and `/register`, so SSR earns nothing. - **Auth supports both cookies (web, same-origin) and bearer tokens (native).** ADR-0015's OAuth2 flow extends with refresh-token rotation for native clients. - **Single-container deployment stays.** The API container also serves the Expo Web static bundle. Self-hoster UX is unchanged: one image, one env file. - **TanStack Query / Form / Table carry over** — headless cores work in RN; only the leaf DOM components get rewritten. ## Linked tickets - ADR: universal Expo client + OpenAPI contract (the decision document) - OpenAPI spec generation from zod + `/api/openapi.json` + CI drift gate - API contract hardening (zod everywhere, error envelope, pagination, versioning) - Token-based auth: bearer + refresh flow for native; cookie path stays for same-origin web - Repo restructure into workspaces (`apps/api`, `apps/client`, `packages/api-client`) - Generated typed API client + TanStack Query hooks package - Expo client scaffolding (Expo Router, RN Web, theming port, i18n, offline) - Port existing screens to Expo Router (Profile, Skills, Experience, Settings, Notes, Login, Register, Home, Offline) - Decommission Next.js UI (`app/(app)/*`, `app/login`, `app/register`, `app/page.tsx`) - Single-container deployment: API serves the Expo Web bundle as a static fallback - Android build (EAS Build or self-hosted) - Linux Flatpak build (Expo Web bundle in a Flatpak shell) ## Cross-epic dependencies - Supersedes #6 (React Native native-app epic). Close #6 when this lands. - Reshapes #3 (PWA shell): the PWA becomes the Expo Web build. Theme tokens (ADR-0023) port over to Expo's runtime — the biggest line item inside the Expo scaffolding ticket. - ADR-0025 (next-intl) gets superseded by an Expo-compatible i18n setup; the `messages/*.json` catalog stays, the runtime swaps. - ADR-0004 (server-side sessions) and ADR-0015 (OAuth2) get extended for bearer-token issuance. - ADR-0012 (TanStack) is reaffirmed. ## Exit criteria - The Next.js app, when run, serves **only** API routes and the static Expo Web bundle. No server-rendered UI. - `GET /api/openapi.json` returns a valid OpenAPI 3.1 document covering every authenticated route. CI fails if a route is missing or the spec disagrees with its zod schema. - The Expo client, built once, runs as the installable PWA *and* as an Android APK pointed at a self-hoster's URL *and* as a Linux Flatpak — all hitting the same generated typed client. - A second user on the same instance still sees none of the first user's data; the cross-user 404 contract continues to hold at the API layer. - `CLAUDE.md` "Shape of the system" and `idea.md` are updated; the old "PWA = same Next.js app" wording is gone.
james closed this issue 2026-06-23 11:56:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#176
No description provided.