Configuration

The sootsim.config.ts surface — module / turboModule / nativeModule resolution, env injection, settings, initialState — with real demo-app examples (Uniswap, Bluesky, Mattermost) and how the config currently reaches the runtime.

sootsim.config.ts (or .js) is the per-project, repo-rooted config file that describes how SootSim should load your app: which native modules to stub, which env vars to inject, which device/appearance settings to default to, and any initial app state.

Scaffold one

terminal

sootsim config init # writes sootsim.config.ts in cwd
sootsim config show # print the current file
sootsim config validate # sanity-check the file

Both forms are accepted:

// sootsim.config.ts — typed via defineConfig
import { defineConfig } from 'sootsim/config'
export default defineConfig({
/* … */
})
// sootsim.config.js — plain CJS, type-annotated via JSDoc
/** @type {import('sootsim/config').SootSimConfig} */
module.exports = {
/* … */
}

Full reference

import { defineConfig } from 'sootsim/config'
export default defineConfig({
// generic Metro module overrides (matched by path fragment).
modules: {
'react-native-analytics': 'noop', // empty module
'react-native-gesture-handler': false, // disable SootSim's builtin, use original
'my-native-lib': { use: 'expo-haptics' }, // redirect to an existing compat stub
'my-native-camera': { file: './stubs/camera.ts' },
'react-native-config': {
inline: { API_URL: 'http://localhost:3000', ENV: 'development' },
},
// path-fragment match: this targets a deep file inside a package
'dist/assets/config.json': {
inline: {
/* … */
},
},
},
// TurboModuleRegistry surface (keyed by TurboModule name).
turboModules: {
RNCAsyncStorage: { file: './stubs/async-storage.ts' },
},
// legacy NativeModules surface (keyed by native module name).
nativeModules: {
RNKeychainManager: { file: './stubs/keychain.ts' },
},
// env vars (process.env.*) — see "Env caveat" below.
env: { API_URL: 'http://localhost:3000', DEBUG: 'true' },
// simulator settings (CLI flags override these per invocation).
settings: { deviceModel: 'iphone-16-pro', colorScheme: 'dark' },
// initial app state passed to the simulator.
initialState: {
authenticated: false,
locale: 'en',
featureFlags: { newOnboarding: true },
},
})

How module / turboModule / nativeModule differ

Three separate slots because the runtime resolves each differently:

  • modules — overrides for any Metro module by path fragment. Keys are matched against the Metro module name (e.g. …/node_modules/react-native-widgetkit/index.js); a key like react-native-widgetkit matches that path. Keys are tried longest-first, so subpath keys override package-level keys.
  • turboModules — keyed by the TurboModule name the guest bundle requests (RNCAsyncStorage, RNFastImageView, …), served through SootSim’s TurboModuleRegistry surface.
  • nativeModules — keyed by the legacy NativeModules name (RNKeychainManager, RNUtils, …), served through the NativeModules.<Name> surface.

If a guest bundle reads TurboModuleRegistry.get('Foo') and you have a Foo key under modules, it will not match — modules is for JS-module paths, turboModules is for TurboModule names.

ModuleResolution kinds

type ModuleResolution =
| 'noop' // empty module
| false // disable SootSim's builtin stub, use the original
| { use: string } // redirect to an existing compat stub
| { file: string } // load a project-local file
| { inline: Record<string, any> } // inline static object as module exports

turboModules and nativeModules additionally accept a direct Record<string, any> for non-URL configs.

Real demo-app examples

Uniswap — simple noop list

/Users/n8/github/uniswap-interface/sootsim.config.ts:

import { defineConfig } from 'sootsim/config'
export default defineConfig({
// env vars the app reads via react-native-dotenv babel plugin.
// these are injected at bundle time, not runtime, so they're only
// useful if sootsim re-bundles. included here for documentation.
env: {
NODE_ENV: 'development',
EXPO_BUILD_CONFIGURATION: 'Debug',
},
modules: {
'@datadog/mobile-react-navigation': 'noop', // analytics — irrelevant in sim
'@shopify/react-native-performance-navigation': 'noop',
'@sparkfabrik/react-native-idfa-aaid': 'noop', // IDFA — irrelevant in browser
'react-native-widgetkit': 'noop', // iOS widgets
'sp-react-native-in-app-updates': 'noop', // in-app updates
},
})

Bluesky — env only, CJS form

/Users/n8/github/bluesky/sootsim.config.js:

/** @type {import('sootsim/config').SootSimConfig} */
module.exports = {
env: {
EXPO_PUBLIC_ENV: 'development',
EXPO_PUBLIC_LOG_LEVEL: 'debug',
EXPO_PUBLIC_LOG_DEBUG: 'session',
},
}

Mattermost — nativeModules + deep inline override

Mattermost reads its server config from dist/assets/config.json inside its own bundle, and ships several legacy NativeModules:

defineConfig({
modules: {
// mattermost patches react-native-keychain's JS entry itself; let it run
// and only provide the native manager seam below.
'react-native-keychain': false,
// override a deep file inside the bundle by path-fragment match.
'dist/assets/config.json': {
inline: {
DefaultServerUrl: 'http://localhost:8065',
DefaultServerName: 'Mattermost Demo',
AutoSelectServerUrl: true,
SentryEnabled: false,
// …
},
},
},
nativeModules: {
RNKeychainManager: { file: './stubs/mattermost/keychain.ts' },
RNUtils: { file: './stubs/mattermost/rn-utils.ts' },
GenericClient: { file: './stubs/mattermost/network-client.ts' },
ApiClient: { file: './stubs/mattermost/network-client.ts' },
WebSocketClient: { file: './stubs/mattermost/network-client.ts' },
},
})

See packages/sootsim/scripts/demo-app-registry.ts for the full Mattermost entry. The same registry has entries for Expensify, Artsy, Joplin, and Rainbow that exercise other parts of the surface.

Env caveat — bundle-time vs runtime

Many React Native projects read env vars at bundle time via Babel plugins (react-native-dotenv, Expo’s EXPO_PUBLIC_* inliner, babel-plugin-transform-inline-environment-variables). Those values are already inlined into the bundle Metro served — env in sootsim.config.ts only sets process.env.* for code that reads env at runtime.

If your code looks like process.env.API_URL after Metro and that string has been replaced by a literal in the bundle, changing env here does nothing. Rebuild the bundle with the new env (restart Metro with the new .env, or set the var in the Metro process) and reload. Bluesky and Uniswap’s configs both document this constraint.

How the config currently reaches the runtime

The runtime config is carried as the ?sootsimConfig=<json> URL query param. applySootSimConfigToUrl(url, config) writes it; readSootSimConfigFromSearchParams reads it back inside the engine; the engine then preloads { file: '…' } modules through the /__sootsim-replacement-module dev-server endpoint and registers inline/turbo/native overrides before any guest module factory runs.

Today the entry points that produce that URL are:

  • The demo-app registry (packages/sootsim/scripts/demo-app-registry.ts) attaches a hardcoded runtimeConfig per known demo (Mattermost, Expensify, Artsy, …). The dev-server scanner wraps the bundle URL with withRuntimeConfig and sootsim open <port> opens that URL.
  • The sootsim open --replace <module>=<file> CLI flag injects a one-off file override without touching a config file.
  • Programmatic callers of applySootSimConfigToUrl (e.g. the electron host or custom scripts) can build the URL themselves.

A project’s on-disk sootsim.config.ts for a non-registry app is, today, read by sootsim config show/validate but is not auto-loaded by sootsim open or the Metro/One plugins — wire it up either by adding the project to the demo registry, by passing --replace flags, or by calling applySootSimConfigToUrl from your own launcher. The file format is the forward-compatible target for auto-load.

Settings reference

Settings can be configured three ways, in order of override priority (highest last):

  1. sootsim.config.ts — project-level defaults
  2. Simulator UI — runtime changes (notification center pull-down)
  3. CLI flags — per-invocation overrides

Device

settingCLI flagvaluesdefault
deviceModel--deviceiphone-se, iphone-15, …, iphone-16-pro-maxiphone-15-pro
orientation--orientationportrait, landscapeportrait

Appearance

settingCLI flagvaluesdefault
colorScheme--themelight, dark, autolight
reduceMotionbooleanfalse
boldTextbooleanfalse
fontSize0.5–2.01.0

Network

settingCLI flagvaluesdefault
networkCondition--networkwifi, lte, fast-3g, slow-3g, offlinewifi

Locale

settingCLI flagvaluesdefault
language--languageISO 639-1 codesen
region--regionISO 3166-1 codesUS

Chrome

settingCLI flagvaluesdefault
showFrame--framebooleantrue
showTouchesbooleanfalse
showStatusBarbooleantrue
showHomeIndicatorbooleantrue
a11yModeoff, default, alwaysdefault
inspectModebooleanfalse

Optional dev-server integration

If you want a stable /__soot URL on the same dev server your team already runs, install the integration that matches your stack:

These integrations expose SootSim on the existing dev server but do not auto-load sootsim.config.ts — see “How the config currently reaches the runtime” above.

Ready to build?

Run your React Native app in the browser. No simulators, no native toolchain, no waiting.

npm i -g sootsim