Stack technique

Partager un backend Convex entre TanStack (web) et Expo (mobile)

Architecture full-stack avec un seul backend Convex pour ton app web TanStack Start et ton app mobile Expo : auth, realtime, queries partagées, différences cookies vs bearer tokens.

KIKillian
··6 min read
Smartphone et ordinateur portable côte à côte représentant une application cross-platform

Tu veux livrer ton SaaS sur le web et sur mobile sans coder un backend deux fois. Bonne nouvelle : avec Convex comme source of truth, TanStack Start pour le web et Expo Router pour le mobile, tu obtiens un setup full-stack cross-platform qui partage 80 % de la logique. Voici l'architecture concrète, avec les vraies différences à gérer.

L'architecture full-stack cross-platform

Une seule source de vérité (Convex), deux clients qui consomment les mêmes queries et mutations. Pas de duplication d'API, pas de schémas désynchronisés, pas d'auth à câbler deux fois.

┌──────────────────────────────────────────────────┐
│              @apexkit/backend (Convex)           │
│  schema · queries · mutations · auth · billing   │
└──────────────┬─────────────────────┬─────────────┘
               │                     │
       ┌───────▼────────┐   ┌────────▼──────────┐
       │  apps/web      │   │  apps/mobile      │
       │  TanStack      │   │  Expo Router      │
       │  Start (SSR)   │   │  (React Native)   │
       └────────────────┘   └───────────────────┘

Les deux apps importent @apexkit/backend comme une dépendance workspace. Elles voient les mêmes types, appellent les mêmes APIs.

Le code partagé (80 % du travail)

Toute la logique métier vit dans packages/backend/convex/. Les deux clients consomment via api.module.function.

// packages/backend/convex/articles.ts
export const listPublished = query({
  args: { limit: v.optional(v.number()) },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("articles")
      .withIndex("by_status", q => q.eq("status", "published"))
      .order("desc")
      .take(args.limit ?? 20);
  },
});
// apps/web/src/routes/blog/index.tsx — TanStack Start
import { useQuery } from "convex/react";
import { api } from "@apexkit/backend/convex/_generated/api";

export default function BlogIndex() {
  const articles = useQuery(api.articles.listPublished, { limit: 20 });
  return <ArticleGrid articles={articles ?? []} />;
}
// apps/mobile/app/(tabs)/blog.tsx — Expo Router
import { useQuery } from "convex/react";
import { api } from "@apexkit/backend/convex/_generated/api";

export default function BlogScreen() {
  const articles = useQuery(api.articles.listPublished, { limit: 20 });
  return <ArticleList articles={articles ?? []} />;
}

Strictement le même appel. Même type retourné. Et — magique — les deux clients reçoivent les updates realtime simultanément. Tu publies un article depuis le dashboard web, l'app mobile l'affiche sans refresh.

Les 20 % qui diffèrent : la couche transport

1. Auth : cookies (web) vs bearer (mobile)

Web : Better Auth stocke la session dans un cookie HTTP-only. ConvexReactProvider ramasse le cookie automatiquement.

Mobile : pas de cookies (mal supportés par React Native). On utilise des bearer tokens stockés dans expo-secure-store (Keychain iOS / Keystore Android), envoyés en header Authorization: Bearer <token>.

// apps/mobile/app/_layout.tsx
import { ConvexReactClient } from "convex/react";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { authClient } from "@/lib/auth-client";

const convex = new ConvexReactClient(
  process.env.EXPO_PUBLIC_CONVEX_URL!,
  { unsavedChangesWarning: false }
);

export default function RootLayout() {
  return (
    <ConvexBetterAuthProvider client={convex} authClient={authClient}>
      <Slot />
    </ConvexBetterAuthProvider>
  );
}

2. SSR (web only)

TanStack Start fait du SSR : le HTML initial inclut déjà les données fetched. Convex supporte ça via convex/nextjs-style integration ou via des loaders TanStack qui appellent l'API Convex HTTP.

Mobile : pas de SSR, juste du SPA classique. Le premier render affiche un skeleton, puis le data arrive.

3. Storage offline

Convex stream le data dès qu'on est online. Pour offline-first sur mobile, tu peux utiliser le hook useOfflineMutation (officiel Convex) qui queue les mutations en local et les rejoue à la reconnexion.

Web rarely needs ça — un SaaS doit être online pour faire ses queries de toute façon.

Smartphone affichant une interface mobile React Native moderne avec données live

Configurer les deux apps

Web (apps/web)

// apps/web/src/router.tsx
import { ConvexReactClient } from "convex/react";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";

const convex = new ConvexReactClient(
  import.meta.env.VITE_CONVEX_URL,
);

export function App({ children }) {
  return (
    <ConvexBetterAuthProvider client={convex} authClient={authClient}>
      {children}
    </ConvexBetterAuthProvider>
  );
}

Mobile (apps/mobile)

# Crée la nouvelle app Expo
pnpm create expo apps/mobile --template tabs
cd apps/mobile

# Ajoute Convex + Better Auth
pnpm add convex @convex-dev/better-auth @better-auth/expo expo-secure-store

Variables d'env Expo : préfixer EXPO_PUBLIC_ pour qu'elles soient embarquées dans le bundle.

Realtime cross-device : le moment magique

C'est le moment où Convex prouve sa supériorité sur un backend classique. Trois scénarios qu'on a tous vécus :

  1. Un user édite un profil depuis l'app mobile. Le dashboard web affiché en parallèle se met à jour sans refresh.

  2. Un admin publie un article depuis le web. La liste sur mobile s'incrémente automatiquement.

  3. Un webhook Stripe upgrade un user. Le bouton « Premium » se déverrouille des deux côtés en même temps.

Zéro code de sync. Tu écris useQuery, Convex s'occupe du reste. C'est ce qu'on voulait de Firebase il y a 10 ans, version mature et typée.

Le typage cross-package

Le package @apexkit/domain contient les types métier purs (pas de runtime). Les deux apps et le backend l'importent :

// packages/domain/src/article.ts
export type ArticleStatus = "draft" | "published" | "archived";

// packages/backend/convex/articles.ts
import type { ArticleStatus } from "@apexkit/domain";

// apps/web et apps/mobile
import type { ArticleStatus } from "@apexkit/domain";

Tu changes un type, TypeScript signale les 3 packages à mettre à jour avant compilation. Plus de drift entre backend et UI.

Pièges spécifiques mobile

Hermes et Intl

Le moteur Hermes (par défaut Expo) renvoie en-US pour Intl.NumberFormat — bug connu. Pour le format de devises, utilise expo-localization pour récupérer la vraie locale device, puis passe-la explicitement à toLocaleString.

Pour que le callback Google OAuth revienne dans l'app mobile, tu déclares un scheme dans app.json (ex: "scheme": "apexkit"), puis Better Auth ouvre apexkit://callback. L'app est réveillée avec le token.

Metro (bundler RN) gère mal les symlinks pnpm. Solution : .npmrc avec node-linker=hoisted dans apps/mobile/.

Le tradeoff à connaître

Un seul backend Convex pour web + mobile = un seul point de défaillance. Si Convex est down, les deux apps sont down. C'est le tradeoff classique du backend partagé.

Mitigation : Convex a un uptime > 99.9 %, et les queries cachées côté client survivent quelques secondes d'indispo. Pour un SaaS B2C ou B2B early-stage, c'est largement acceptable.

Récap : ce que tu obtiens

  • Un schéma DB unique, typé end-to-end

  • Queries/mutations partagées entre web et mobile

  • Auth unifiée (Better Auth) cookies + bearer

  • Realtime cross-device automatique

  • Billing Stripe centralisé

  • 80 % du code métier réutilisé

Le ratio temps-investi / value-obtenue est imbattable pour un solo-founder qui veut livrer rapidement web + mobile.

ApexKit livre exactement cette stack

Si tu veux récupérer cette architecture déjà câblée — TanStack Start + Expo Router + Convex partagé + Better Auth web/mobile + Stripe + tests — c'est exactement le scope d'ApexKit. L'offre « Web + Mobile » te file les deux apps pré-configurées, prêtes à déployer sur VPS ou Vercel et à publier sur EAS Build.

Une journée d'install vs deux mois de plomberie. Tu choisis.


Construire ton SaaS encore plus vite

ApexKit est le template full-stack TypeScript que j'ai construit pour livrer un SaaS en une semaine : auth passwordless, multi-tenant, billing Stripe, blog, docs et dashboard admin — câblés ensemble avec TanStack Start et Convex.

Découvrir ApexKit →

Web seul ou Web + mobile (Expo), source-of-truth Convex partagée, prêt à déployer sur VPS ou Vercel en quelques minutes.

Testimonials

Command palette

Search and run actions.