React Native renderer (iOS & Android)
The React Native renderer (frontend/app/react-native, Expo SDK 57 / React Native 0.86, TypeScript)
turns any Mateu backend into a native iOS and Android app. It speaks the same
POST /mateu/v3/sync/{route} wire as the web renderers, so the backend needs no changes — one
server serves web, desktop and mobile at once. It is a fully supported renderer: every
component type of the wire has a native rendering (see the
coverage table), and CI fails the build if a
new wire type arrives without one.
For the architecture and the other native renderer see Desktop & Mobile.
1. Point it at a backend
Section titled “1. Point it at a backend”Three ways, from quickest to production:
| Situation | How the app finds its backend |
|---|---|
Local development (npm run web, Expo Go, emulator) | http://<dev host>:8594 — the host is the Expo dev server’s (hostUri, so a phone on your Wi-Fi reaches your laptop), the port is EXPO_PUBLIC_MATEU_BACKEND_PORT (default 8594, demo/demo-front-office) |
| Any build pointing at a fixed backend | EXPO_PUBLIC_MATEU_REGISTRY_URL + EXPO_PUBLIC_MATEU_APP_ID → the app registry |
| Store installables | the same two values, set in the EAS environment of the build profile (see store builds) |
cd frontend/app/react-nativenpm ciEXPO_PUBLIC_MATEU_BACKEND_PORT=8595 npm run web # demo-admin-panel on :8595, in a browsernpm start # QR code for Expo Go on a real phoneEXPO_PUBLIC_MATEU_ROUTE=/some/route boots straight into a route instead of the home screen; in
the browser build, ?route=/some/route on the page URL does the same without a rebuild.
In a browser (npm run web) the backend must allow the Expo origin. The page is served from
another origin than the backend, and Mateu’s CORS is off by default — without this the app stops
at “Can’t reach the server” and the browser console shows a CORS error:
java -jar target/demo-admin-panel-0.0.1-SNAPSHOT.jar --mateu.cors.allowed-origins=http://localhost:8081Expo Go, simulators and installables are not browsers: they need no CORS setting.
2. The app registry
Section titled “2. The app registry”An installable carries only a registry URL and an app id. At boot it fetches
{registryUrl}/{appId}.json (or the URL with {appId} substituted) and gets everything else, so
retargeting a backend, changing launch parameters, forcing an upgrade or switching the identity
provider is a registry edit — not a store release:
{ "appId": "front-office", "baseUrl": "https://apps.example.com/front-office", "parameters": { "tenantId": "1111" }, "requiredRendererVersion": "1.0.0", "storeUrl": { "android": "https://play.google.com/…", "ios": "https://apps.apple.com/…" }, "auth": { "type": "oidc", "issuer": "https://login.example.com/realms/acme", "clientId": "mateu-mobile", "scopes": ["openid", "profile", "email", "offline_access"] }}parametersare seeded into theappStateof every request.requiredRendererVersiongates the boot: an older installable shows Update required, tries an over-the-air update (EAS Update) and falls back tostoreUrl.authturns sign-in on — next section.
3. Signing in to a secured backend
Section titled “3. Signing in to a secured backend”A secured Mateu backend resolves identity (@EyesOnly, @ReadOnlyUnless, @DisabledUnless,
HttpRequest roles) from a JWT sent as Authorization: Bearer <token>. The native renderer sends
it on every Mateu request (and on the AI chat), from a pluggable token provider:
- Built-in OIDC — declare
authin the registry entry (or, in dev,EXPO_PUBLIC_MATEU_OIDC_ISSUER/_CLIENT_ID/_SCOPES/_AUDIENCE). The app runs the Authorization Code flow with PKCE in the system browser (expo-auth-session), keeps the tokens in the keychain / keystore (expo-secure-store;localStorageon web), refreshes the access token before it expires, and shows a Sign in screen before the first screen ("loginOnStart": falsesigns in only when a request comes back 401). A Sign out entry appears at the bottom of the drawer. Works with any standards-compliant provider (Keycloak, Entra ID, Auth0 — add"audience"—, Okta, Cognito…): it only needs the issuer’s discovery document. - Your own login — register any provider:
setTokenProvider({ getToken, refresh, login, logout })fromsrc/core/auth.ts.
Register these redirect URIs as allowed for the client at the identity provider:
mateu://auth (iOS/Android — the app’s scheme is mateu) and <web origin>/auth (expo web).
Session expiry follows the web renderers’ contract: a 401 gives the app one chance to
re-authenticate — an explicit onSessionExpired(handler) (src/core/sessionGuard.ts) wins;
otherwise the provider refreshes silently, then prompts — and the original request is retried
once, so a half-filled form survives. Concurrent 401s share a single re-authentication.
Every installation also sends its own X-Session-Id (a random UUID created on first launch and
kept in secure storage), so two devices never share a server-side session.
4. Store builds
Section titled “4. Store builds”Builds go through EAS (npx eas-cli login once).
eas.json has three profiles; each maps to an EAS environment and an EAS Update channel
of the same name:
| Profile | Use | Network | App id |
|---|---|---|---|
development | internal APK against a LAN backend | cleartext http allowed (Android usesCleartextTraffic, iOS local networking) | io.mateu.app.dev |
preview | internal APK / ad-hoc for QA | https only | io.mateu.app |
production | Play Store .aab / App Store .ipa | https only | io.mateu.app |
# registry coordinates per environment — nothing environment-specific is committednpx eas-cli env:create --environment preview --name EXPO_PUBLIC_MATEU_REGISTRY_URL --value https://registry.example.comnpx eas-cli env:create --environment preview --name EXPO_PUBLIC_MATEU_APP_ID --value front-office
npm run build:apk # preview APKnpm run build:android # production .aabnpm run build:ios # production .ipanpm run submit:android # Play Store, internal tracknpm run submit:ios # App Store ConnectStore credentials are never in the repository. Upload them once to EAS
(npx eas-cli credentials: the Play service-account JSON and the App Store Connect API key), or
provide them on the submitting machine (EXPO_ASC_API_KEY_PATH, EXPO_ASC_KEY_ID,
EXPO_ASC_ISSUER_ID). Over-the-air updates: npx eas-cli update --channel production
publishes a new JS bundle to production installables; runtimeVersion follows the app version,
so an update only reaches binaries it is compatible with. The camera permission is the only
runtime permission requested (for @PhotoCapture); the microphone is explicitly not.
5. Compatibility
Section titled “5. Compatibility”The app declares the oldest wire it supports (MIN_WIRE_VERSION = "3.0",
src/core/wireVersion.ts). Every response carries wireVersion: a server speaking another major
version (or an older minor) gets a one-time Version mismatch warning instead of screens that
silently render wrong; a newer minor is fine (the wire is additive within a major).
6. Checks
Section titled “6. Checks”npm test # unit tests (node --test)npm run typecheck # tsc --noEmitnpm run parity # every wire component type has a native renderer; parity.md in syncnode scripts/wire-fixture-server.mjs 18600 # a page of rarely-used component types, for eyeballingRequest and response bodies are logged to the console only in development builds (__DEV__).