Simon Boisset
Back to blog

How I use Expo to ship white-label on-premise mobile apps

June 25, 2026

How I use Expo to ship white-label on-premise mobile apps

Shipping a white-label mobile app should not require cloning the whole product: Expo makes it possible to keep one core and publish clean variants.

On a product like Questovery, the same React Native foundation may need to serve several contexts: the standard SaaS app, an internal preview app, a dedicated client app, or an on-premise variant with its own identity. The common mistake is to copy the mobile application for every client. It works at first, then every fix, native dependency and store release becomes repeated work.

The approach I prefer is different: a shared mobile shell, thin variants, and Expo/EAS configuration that carries the native identity of each build.

The real problem with white-label apps

A white-label app is not just a logo change. It can involve:

  • a different app name
  • dedicated iOS bundle identifiers and Android package names
  • a specific deep link scheme
  • distinct native assets
  • different associated domains
  • API, web and PowerSync endpoints for the right instance
  • isolated EAS Update channels
  • sometimes a separate store listing and release strategy

If all of that is duplicated across app folders, the debt appears quickly. You end up maintaining several almost-identical apps, with accidental differences that are hard to audit.

In Questovery, the goal is to keep common behavior in shared packages, while app folders only carry configuration, branding, EAS profiles and publishing metadata.

One shared shell, explicit variants

The structure looks like this:

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

The mobile shell owns screens, navigation, authentication, runtime services, analytics and the game experience. The apps/mobile-on-prem folder only decides which variant to load.

A variable such as APP_VARIANT selects the active brand:

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

That variant can define the app name, native identifiers, assets, domains and endpoints. Business behavior stays in the shell and shared packages.

That is the key point: the brand is configuration, not a product fork.

app.config.ts as the native assembly point

Expo is a good fit for this model because app.config.ts can be dynamic. At build time, the active variant is resolved and native fields are injected for iOS, Android and EAS.

Simplified example:

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

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

In this model, the Expo project can remain shared by the app folder, while operational isolation happens elsewhere: bundle ids, package names, schemes, assets, EAS profiles, channels, endpoints and store listings.

This avoids turning the Expo project id into a client-facing business property. The EAS project belongs to the app folder. The variant belongs to the build.

eas.json owns the environments

Then eas.json describes the available profiles. A variant can have dev, staging and production profiles, each with its own public variables:

{
  "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"
      }
    }
  }
}

There are two categories worth keeping separate:

  • APP_VARIANT is a native build-time variable used to resolve the brand
  • EXPO_PUBLIC_* values are bundle/update-time inputs embedded in JavaScript or an OTA update

That separation prevents native identity, runtime endpoints and product rules from being mixed together.

OTA updates: channels, branches and variables matter

With EAS Update, OTA updates are very useful for white-label variants, but they should not become a dangerous shortcut.

A simple rule works well:

  • if the change affects the native binary, create a new build
  • if the change only affects JavaScript compatible with the existing runtime, publish an OTA update

Modern workflows can compute a fingerprint, look for an existing build, then choose between a native build and an update. This is especially useful for on-premise apps where some variants should not build on every commit.

One detail has saved me from real mistakes: fingerprint, update and channel-linking jobs must receive the same variables as the EAS profile they target. An update job should not assume it automatically inherits everything from the build profile.

In practice, I prefer to test that contract: if a workflow publishes an OTA for APP_VARIANT=client, it must also carry EXPO_PUBLIC_API_URL, EXPO_PUBLIC_WEB_URL, EXPO_PUBLIC_INSTANCE_KEY and any other values required by that variant.

When to reuse a shared on-prem app

Not every white-label brand deserves a new mobile folder.

I keep a shared on-prem app when:

  • product behavior is identical
  • the difference is mostly identity, endpoints, catalog or publishing
  • clients can share the same shell and navigation rules
  • divergences remain testable through configuration

I create a dedicated app when:

  • the client has a real functional divergence
  • the store strategy needs strong autonomy
  • the data or sync model must evolve separately
  • configuration risk becomes higher than the cost of a dedicated folder

This is an architecture decision, not only a build decision.

Why this matters for Questovery

Questovery lets organizers create and run trails, scavenger hunts, guided visits and field activities with a mobile app for participants. For the dedicated offer, the point is not just adding a logo: the app has to feel coherent, isolated, connected to the right backend and maintainable over time.

That is exactly where Expo and EAS are useful: keep a solid common foundation, then industrialize variants through native configuration, channels and workflows.

For the product side of that model, I documented the dedicated offer here: Questovery Dedicated, white-label mobile app and isolated environment.

The real benefit

White-label becomes sustainable when it is treated as a platform problem:

  • client differences are explicit
  • builds are reproducible
  • OTA updates stay isolated
  • the mobile shell remains shared
  • on-premise choices are visible in configuration
  • tests can verify workflow contracts

Expo does not remove the complexity. It gives you a good place to put it.

Simon Boisset

I help teams move away from fragile mobile stacks, stabilize releases, and regain control of Expo/EAS. Questovery, my geolocated treasure hunt product, keeps that work grounded in real usage.

Book a call