ReadyNative

Config

readynative.config.ts fields, app.config.ts variants and the generated .readynative.json.

readynative.config.ts

The one file you edit. It must stay importable under plain Node (no React Native imports; only types from src/), because app.config.ts reads it while Expo resolves the native config. The shape is ReadyNativeConfig from src/lib/readynative-config.ts.

BlockFieldMeaning
appnameDisplay name under the icon; variants append (Dev) / (Preview)
slugExpo project slug (EAS)
schemeDeep-link scheme, e.g. readynative → readynative://
ownerExpo account / organisation slug owning the EAS project; empty → omitted
easProjectIdEAS project id printed by bunx eas-cli init; feeds extra.eas.projectId + updates.url
ios.bundleIdiOS bundle identifier; variants append .dev / .preview
android.packageAndroid application id; same suffix rule
brandprimaryHex colour; overrides the light primary token
accentHex colour; exposed as the brandAccent token
radiusBase corner radius in px (radius.lg); the other steps derive from it
fontFont family name for the fontFamily token; you load the font file (see below)
urlswebsite, support, privacy, termsUsed by the settings screen; support may be a mailto: URL
storeappStoreIdNumeric App Store id for review prompts / store links
playPackagePlay Store package (usually equals app.android.package)
linksdomainBare host for iOS universal links / Android App Links (app.example.com). Empty → links off
appleTeamId10-character Apple Team ID; prefixes the AASA app ids
androidCertFingerprintsSHA-256 fingerprints of the Android signing certs (eas credentials prints them)
featuresreviewPromptAsk for a store review (expo-store-review)
updatesBannerShow the "update available" banner backed by expo-updates

Who reads it:

  • app.config.ts - names, bundle ids, scheme, associatedDomains / intentFilters.
  • src/theme/tokens.ts - brand → colour, radius and fontFamily tokens (then bun run gen:theme).
  • The settings screen - urls, store, features.
  • bun run doctor - flags the com.acme.app placeholders, empty URLs / store ids, a links.domain without appleTeamId / androidCertFingerprints, and an unlinked EAS project (app.easProjectId empty and no EAS_PROJECT_ID) or a missing updates.url.
  • bun run gen:links - links → public/.well-known/apple-app-site-association and assetlinks.json.

Import it in app code through @/lib/readynative (ESLint blocks direct imports of readynative.config).

Brand font

brand.font is only a name. Nothing in the tree loads a font file, so until you do, the name points at a font the device doesn't have and text falls back to the system font. Put the file under assets/fonts/ and load it under exactly the name in brand.font, holding the splash screen until it's ready:

src/app/_layout.tsx
import { useFonts } from "expo-font";

export default function RootLayout() {
  const [fontsLoaded] = useFonts({
    Inter: require("@/assets/fonts/Inter-Regular.ttf"),
  });

  useEffect(() => {
    if (!fontsLoaded) return;
    hydrateThemeMode().finally(() => {
      SplashScreen.hideAsync().catch((err: unknown) => {
        console.warn("[splash] hideAsync failed", err);
      });
    });
  }, [fontsLoaded]);

  if (!fontsLoaded) return null;
  // …the existing return
}

expo-font is already a dependency and useFonts works in Expo Go. The token reaches text through the stylesheet and unistyles kits; with NativeWind set the family in your Tailwind theme, with Tamagui in tamagui.config.ts. Each weight is its own file - load Inter-Bold under its own name if you need real bold on Android.

links is always wired but off until domain is set:

  1. Set domain, appleTeamId and androidCertFingerprints.
  2. bun run gen:links writes the two well-known files for the prod, preview and dev bundle ids into public/.well-known/. Host them over https at that domain - expo export -p web ships public/ as-is, so the web build living there is enough.
  3. Rebuild the native app: associatedDomains and intentFilters are native config that app.config.ts emits only when links.domain is non-empty.

Test on a simulator with npx uri-scheme open https://<domain>/x --ios (or --android).

app.config.ts and variants

app.config.ts is hand-written and stays that way; modules never edit it. It derives three variants from APP_VARIANT (default dev):

APP_VARIANTName suffixBundle id / package suffixEAS profile
dev (Dev).devdevelopment
preview (Preview).previewpreview
prod(none)(none)production

So com.acme.app becomes com.acme.app.dev for the dev build and all three can be installed side by side. eas.json sets APP_VARIANT and EXPO_PUBLIC_APP_VARIANT per profile; locally they come from .env (.env.example starts with APP_VARIANT=dev). The value is also exposed as extra.appVariant and validated in src/lib/env.ts.

Other fixed choices in app.config.ts: runtimeVersion: { policy: "appVersion" }, typed routes and the React Compiler on (experiments), and web.output: "static".

owner, extra.eas.projectId and updates.url all come from readynative.config.ts: easProjectId (or EAS_PROJECT_ID, which wins) becomes extra.eas.projectId and updates.url: https://u.expo.dev/<projectId>. While the id is empty the whole updates key is omitted - an empty URL would break expo-updates. owner is omitted when empty. See Ship to TestFlight.

Module app patch

Modules contribute native config through module.json → app.expo (config plugins, extra keys). setup merges every selected module's patch into .readynative.json → modules.app, and app.config.ts applies it at load time: plugins are appended unique by plugin id, other keys are deep-merged. Example: push/expo-notifications adds the expo-notifications plugin, auth/supabase adds expo-apple-authentication.

.readynative.json

Written by setup, read by app.config.ts and doctor:

{
  "version": 1,
  "preset": "default",
  "selection": { "ui": "nativewind4", "data": "react-query", "...": "..." },
  "applied": true,
  "modules": {
    "app": { "expo": { "plugins": ["expo-apple-authentication"] } },
    "postSetup": ["Supabase: create a project, …"],
    "doctor": [{ "type": "env", "keys": ["EXPO_PUBLIC_SUPABASE_URL"], "module": "supabase" }]
  },
  "keepModules": true
}

preset is null when the selection matches no preset. keepModules: false plus a different selection on the next setup run is the error that tells you modules/ is gone.

Environment keys

.env (gitignored) holds every key; .env.example (generated) lists them per module with a # docs: link. Rules:

  • Public keys start with EXPO_PUBLIC_ and are the only ones rendered into src/lib/env.ts, each as z.string().optional() - a missing key degrades the module ("Configure X"), it never crashes at import time.
  • server: true keys (STRIPE_SECRET_KEY, BETTER_AUTH_SECRET, …) have no prefix, live under the # server (never EXPO_PUBLIC) section, are read with process.env.X only in src/app/api/** / src/server/**, and never reach the bundle.
  • bun run doctor checks every required: true key plus explicit doctor entries.

On this page

Get ReadyNative