The app is backend-agnostic by design. You have two clean paths.
Path A, keep the bundled backend
apps/api is a Hono and Drizzle backend on SQLite, with auth, /me, devices, uploads, payments, and webhooks already implemented. It runs through tsx, so there is no build step, and it ships a Dockerfile for deployment.
pnpm --filter api db:migrate
pnpm --filter api devOutgrowing a single SQLite file? docs/swap-to-postgres.md in the repository documents the change.
Endpoints it already serves: /healthz, /auth/* (sign-up, sign-in, refresh, sign-out, forgot and reset password, Apple, Google), /me (read, update, delete), /devices, /uploads, /payments, /webhooks.
Path B, bring your own backend
Already running Django, Rails, FastAPI, or Node? Then:
- Delete
apps/api. - Set
EXPO_PUBLIC_API_URLinapps/mobile/.envto your server. - Implement the endpoints described in
docs/backend-contract.md.
The contract is small. At minimum you need sign-up, sign-in, token refresh, and GET /me. Devices, uploads, and payments are only needed if you use those features.
Skip backend auth entirely
Use a hosted provider and your backend only has to verify their tokens. See Choosing an Auth Provider for what that actually involves.
How the app talks to either one
Every request goes through apiFetch in packages/api-client, which adds the auth header, validates the response against a zod schema, and turns failures into a normalised ApiError. TanStack Query sits on top for caching, retries, and refetching.
Swapping backends never touches your screens. It changes one env var and, if your shapes differ, the schemas in packages/api-client/src/schemas.
Writes that survive a bad connection
Mutations go through TanStack Query's onlineManager, bound to NetInfo. When the device goes offline, writes pause instead of failing, and flush when the connection returns. docs/recipes/offline-queue.md covers the details.
Try this one on a device, not in a browser
NetInfo's web build reports the connection as online regardless of what the browser is doing, so on the web target the banner never changes and a queued write just fails. Use airplane mode on a real device or a simulator to see the queue actually hold and flush.