Simon Boisset
Retour au blog

Comment j’utilise Expo pour publier des apps marque blanche on-premise

25 juin 2026

Comment j’utilise Expo pour publier des apps marque blanche on-premise

Publier une app mobile marque blanche ne devrait pas imposer de cloner tout le projet : Expo permet de garder un socle commun et des variantes propres.

Sur un produit comme Questovery, une même base React Native doit pouvoir servir plusieurs contextes : l’app SaaS standard, une app de preview interne, une app dédiée à un client, ou une variante on-premise avec sa propre identité. Le piège classique consiste à copier l’application mobile pour chaque client. Ça marche au début, puis chaque correctif, chaque dépendance native et chaque mise à jour store devient une opération répétée.

L’approche que je préfère est différente : un shell mobile partagé, des variantes fines, et une configuration Expo/EAS qui porte l’identité native de chaque build.

Le problème réel des apps marque blanche

Une app marque blanche n’est pas seulement un changement de logo. Elle peut impliquer :

  • un nom d’application différent
  • un bundle identifier iOS et un package Android dédiés
  • un scheme deep link spécifique
  • des assets natifs distincts
  • des domaines associés différents
  • des endpoints API, web et PowerSync propres à l’instance
  • des channels EAS Update isolés
  • parfois une fiche store et une stratégie de publication séparées

Si tout cela est dupliqué dans des dossiers d’app différents, la dette arrive vite. On finit par maintenir plusieurs apps presque identiques, avec des divergences accidentelles difficiles à auditer.

Dans Questovery, l’objectif est plutôt de garder le comportement commun dans les packages partagés, et de laisser les dossiers d’app porter uniquement la configuration, la marque, les profils EAS et les métadonnées de publication.

Un shell partagé, des variantes explicites

La structure ressemble à ceci :

apps/mobile-on-prem
  App.tsx
  app.config.ts
  eas.json
  src/on-prem-brands.js
  assets/branding/*
  .eas/workflows/*

packages/mobile-shell
packages/sdk
packages/common

Le shell mobile contient les écrans, la navigation, l’authentification, les services runtime, l’analytics et l’expérience de jeu. Le dossier apps/mobile-on-prem choisit seulement quelle variante charger.

Une variable comme APP_VARIANT sélectionne la marque active :

const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);

Cette variante peut ensuite définir le nom de l’app, les identifiants natifs, les assets, les domaines et les endpoints. Le code métier, lui, reste dans le shell et les packages communs.

C’est le point important : la marque est une configuration, pas un fork produit.

app.config.ts comme point d’assemblage natif

Expo est très pratique pour ce modèle parce que app.config.ts peut être dynamique. Au moment du build, on résout la variante active, puis on injecte les champs natifs attendus par iOS, Android et EAS.

Exemple simplifié :

export default context => {
  const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);

  return buildExpoConfig(
    {
      ...brandConfig.brand,
      easProjectId: 'shared-project-id',
      slug: 'questovery-on-premise',
    },
    version,
  )(context);
};

Dans ce modèle, le projet Expo peut rester commun au dossier d’app, tandis que l’isolation opérationnelle se fait ailleurs : bundle ids, package names, schemes, assets, profils EAS, channels, endpoints et fiches store.

Ce choix évite de transformer l’identifiant de projet Expo en propriété métier du client. Le projet EAS appartient au dossier d’app. La variante appartient au build.

eas.json porte les environnements

Ensuite, eas.json décrit les profils disponibles. Une variante peut avoir un profil dev, staging et production, chacun avec ses propres variables publiques :

{
  "build": {
    "client-staging": {
      "extends": "staging",
      "channel": "onprem-client-staging",
      "env": {
        "APP_VARIANT": "client",
        "EXPO_PUBLIC_EAS_ENV": "preview",
        "EXPO_PUBLIC_INSTANCE_KEY": "client-instance",
        "EXPO_PUBLIC_API_URL": "https://api-staging.client.example.com",
        "EXPO_PUBLIC_WEB_URL": "https://staging.client.example.com",
        "EXPO_PUBLIC_POWERSYNC_URL": "https://powersync-staging.client.example.com"
      }
    }
  }
}

Il y a deux catégories à bien distinguer :

  • APP_VARIANT est une variable de build native, utilisée pour résoudre la marque
  • les EXPO_PUBLIC_* sont des entrées bundle/update-time, embarquées dans le JavaScript ou dans une update OTA

Cette séparation évite de mélanger l’identité native, les endpoints runtime et les règles produit.

OTA : attention aux channels, branches et variables

Avec EAS Update, les OTA sont très utiles pour les variantes marque blanche, mais elles ne doivent pas devenir un raccourci dangereux.

Une règle simple fonctionne bien :

  • si le changement touche le binaire natif, on refait un build
  • si le changement touche uniquement le JavaScript compatible avec le runtime existant, on publie une update OTA

Les workflows modernes peuvent calculer un fingerprint, chercher un build existant, puis choisir entre build natif et update. C’est particulièrement efficace pour des apps on-premise où certaines variantes ne doivent pas builder à chaque commit.

Le détail qui m’a déjà évité des erreurs : les jobs fingerprint, update et les scripts de liaison de channel doivent recevoir les mêmes variables que le profil EAS concerné. Un job d’update ne doit pas supposer qu’il hérite automatiquement de tout ce qui est dans le profil de build.

En pratique, je préfère tester ce contrat : si une workflow publie une OTA pour APP_VARIANT=client, elle doit aussi porter les EXPO_PUBLIC_API_URL, EXPO_PUBLIC_WEB_URL, EXPO_PUBLIC_INSTANCE_KEY et autres valeurs nécessaires à cette variante.

Quand réutiliser une app on-prem partagée

Toutes les marques blanches ne méritent pas un nouveau dossier mobile.

Je garde une app on-prem partagée quand :

  • le comportement produit est identique
  • la différence porte surtout sur l’identité, les endpoints, le catalogue ou la publication
  • les clients peuvent partager le même shell et les mêmes règles de navigation
  • les divergences restent testables par configuration

Je crée une app dédiée quand :

  • le client a une vraie divergence fonctionnelle
  • la stratégie store impose une autonomie forte
  • le modèle de données ou de synchronisation doit évoluer séparément
  • le risque de configuration devient plus élevé que le coût d’un dossier dédié

C’est une décision d’architecture, pas seulement une décision de build.

Pourquoi c’est utile pour Questovery

Questovery permet de créer et exploiter des parcours, jeux de piste, visites guidées et activités terrain avec une app mobile pour les participants. Pour l’offre dédiée, le sujet n’est pas seulement d’apposer un logo : il faut livrer une expérience mobile cohérente, isolée, connectée au bon backend, et maintenable dans le temps.

C’est exactement le genre de cas où Expo et EAS sont efficaces : on garde un socle commun solide, puis on industrialise les variantes autour de la configuration native, des channels et des workflows.

Pour voir le produit côté usage, j’ai documenté l’offre dédiée ici : Questovery Dédié, app mobile en marque blanche et environnement isolé.

Le vrai bénéfice

La marque blanche devient durable quand elle est traitée comme un problème de plateforme :

  • les différences client sont explicites
  • les builds sont reproductibles
  • les updates OTA restent isolées
  • le shell mobile reste commun
  • les choix on-premise sont visibles dans la configuration
  • les tests peuvent vérifier les contrats de workflow

Expo ne supprime pas la complexité. Il donne surtout un bon endroit pour la ranger.

Simon Boisset

J'aide les équipes à sortir d'une stack mobile fragile, stabiliser leurs livraisons et reprendre la main sur Expo/EAS. Questovery, mon produit de jeux de piste géolocalisés, nourrit ce travail au quotidien.

Prendre rendez-vous