One Backend, Two Clients: Architecture for a Next.js Web App and an Expo Mobile App
How to share an API, validation schemas and data-fetching patterns between a Next.js web app and an Expo / React Native app without duplicating business rules.
When a product needs a website and a phone app, the tempting shortcut is to build two separate applications that each talk to the database in their own way. It works until the first rule changes and has to be changed twice, subtly differently. A sturdier approach treats the backend as the single place where rules live and builds both clients as thin consumers of it.
Put the rules behind an API
The shape that scales is a standalone API service — for example Node.js with Express — that owns authentication, authorisation, validation and the database. The web app and the mobile app are both clients of that API.
┌────────────┐ ┌────────────┐ ┌─────────────┐
│ Next.js │ │ Expo / RN │ │ Admin app │
│ web app │ │ mobile app │ │ (Next.js) │
└─────┬──────┘ └─────┬──────┘ └──────┬──────┘
└────────────────┼─────────────────┘
▼
┌───────────────┐
│ REST API │ auth · rules · validation
└───────┬───────┘
▼
┌───────────────┐
│ MongoDB │
└───────────────┘Next.js can also host API routes, and for a site with a single client that is often the right call. Once a second client with a different release cycle appears — an app store build cannot be rolled back in minutes — a dedicated API gives you a stable, versionable contract. The cost is an extra service to deploy and secure.
Authentication that both clients can use
Browsers and native apps store credentials differently, and a design that favours one makes the other awkward.
- Web: keep the refresh token in an
HttpOnly,Secure,SameSitecookie so scripts cannot read it. Keep short-lived access tokens in memory. - Mobile: there are no cookies to rely on. Store tokens in the platform keychain through
expo-secure-store, not in plain async storage. - Both: short-lived access tokens, rotating refresh tokens, and the ability to revoke a session server-side.
The API can accept an access token in an Authorization: Bearer header from both clients and keep cookie handling as a web-specific layer on top.
Share contracts, not UI
Web and native UIs are different. Share what is genuinely the same: the types and validation of the data crossing the API. A schema library like Zod gives you both runtime validation and TypeScript types from one definition:
// shared/schemas.ts
import { z } from "zod";
export const submitAnswerSchema = z.object({
quizId: z.string(),
answers: z.record(z.string(), z.string()),
});
export type SubmitAnswerInput = z.infer<typeof submitAnswerSchema>;The server validates requests with it. Clients use the inferred type to build requests and, in forms, can reuse the schema for instant feedback. Whether the shared code lives in a monorepo package or is copied by a build step, the principle is the same: one definition of the contract.
A shared data-fetching pattern
TanStack Query works on both React (web) and React Native, which means the same mental model, hooks and caching behaviour on both platforms. A typical query hook looks identical in either app apart from the HTTP client's base configuration:
export function useChapterProgress(subjectId: string) {
return useQuery({
queryKey: ["progress", subjectId],
queryFn: () => api.get(`/subjects/${subjectId}/progress`).then((r) => r.data),
staleTime: 60_000,
});
}Mutations invalidate the relevant query keys so that completing a lesson on the phone updates the dashboard the next time the web app focuses. Because the server is the source of truth, the two clients converge without special sync code.
Where the web app uses the server directly
A Next.js web app does not have to behave like a mobile client. Public, indexable pages — a course catalogue or a lesson overview — are best rendered on the server and can call the same data layer or API during render, which gives crawlers real HTML (see Server vs Client Components in Next.js). Interactive, signed-in areas then use client-side queries. The mobile app has no such split; everything is a client.
Differences you must design for
- Offline and flaky networks. Phones lose connectivity. Cache reads, queue or retry writes that are safe to repeat, and make progress updates idempotent so retries do not duplicate state.
- Media. Video is usually delivered as HLS streams, which both a web player and the native player can consume. Large uploads from an admin tool benefit from a resumable protocol so a dropped connection does not restart a multi-gigabyte upload.
- Payments. App stores have their own purchase rules for digital goods on mobile, which differ from web payments. Keep the rule — "this user now has access" — in the backend, and let each platform's purchase flow feed into it. The entitlement approach in Cart, Checkout and Access Entitlements is designed for exactly this.
- Releases. A web deploy reaches everyone immediately; an app update does not. Version the API, keep changes additive, and give the app a way to learn that it is too old to work and must update.
- Push notifications. Store device tokens per user and device on the server, and handle token rotation and invalid tokens when sending.
Real-time updates
If features need live updates — notifications, live counts, collaborative views — a WebSocket layer such as Socket.IO works from both the browser and React Native. When running more than one server instance, add a shared adapter (Redis is the usual choice) so events reach clients connected to different instances.
Observability
With several clients on one backend, errors need context. Add an error-tracking tool to each client and the API, tag events with the app and version, and log request IDs end to end. When something fails on a phone, you want to find the matching server log line quickly.
A few rules of thumb
- Rules live in the API, never only in a client.
- Authorisation is checked on the server for every protected request.
- Share schemas and types; do not force shared UI.
- Make writes idempotent so retries are safe.
- Version the API and prefer additive changes.
- Assume the mobile client is out of date.
This is the architecture behind multi-client products such as E2A Learning, which pairs a Next.js web app, an admin app and an Expo mobile app with a shared backend. For the data side of the same system, see Designing an LMS Data Model.
Related articles
- Cart, Checkout and Access Entitlements for Course Platforms
How to separate orders, payments and access in a course platform — bundles, idempotent payment confirmation, and a single entitlement check that decides who sees what.
- Server vs Client Components in Next.js: Where to Draw the Boundary
How to decide what runs on the server and what ships to the browser in the Next.js App Router, with patterns for forms, interactivity and passing data across the boundary.
- Structuring a Next.js App Router Project for Production
A practical layout for Next.js App Router projects — routes, data access, shared components and configuration — and the reasoning behind each boundary.