The app is backend-agnostic by design. You have two clean paths.
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.
Outgrowing 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 .
Already running Django, Rails, FastAPI, or Node? Then:
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.
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 .
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.