Configuration

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, any initial app state, and whether this repo needs a fixed engine version.

Create one

Create sootsim.config.ts in the repo root only when you need a fixed runtime or advanced module, environment, or simulator overrides. It is not required for normal sootsim open <port> usage.

From a monorepo root, SootSim reads package.json#workspaces and automatically uses the config for the only React Native app it finds. It never guesses when multiple apps match. The fallback scan is shallow, time-limited, and skips dependencies, build output, native projects, fixtures, examples, dot directories, and symlinks.

The smallest version selection has no import and does not add SootSim to the app’s dependencies:

// sootsim.config.ts
export default {
runtimeVersion: '0.1.845',
}

Remove the field to follow the current stable runtime. Repos that already have sootsim as a dependency may optionally use defineConfig for editor types, but the global CLI does not require a per-app install.

Full reference

export default {
// 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.

Examples

Simple noop list

Silence native modules that are irrelevant in the simulator (analytics, IDFA, widgets, in-app updates):

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
},
})

Env only, CJS form

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

nativeModules + deep inline override

Some apps read config from a JSON file deep inside their own bundle and ship several legacy NativeModules. You can override a deep file by path fragment and seam the native managers:

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' },
},
})

The three slots compose freely: redirect JS modules by path, seam TurboModules and legacy NativeModules by name, and inline static config a package reads from its own bundle.

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, so 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 reaches the runtime

The CLI carries your config to the engine when it opens your app, and the engine applies the module / turbo / native overrides before any guest module factory runs. Env vars that were already inlined at bundle time can’t be changed here (see the caveat above).

runtimeVersion is handled by the local host before the engine loads. SootSim installs that runtime on demand and serves the app from a version-specific localhost origin. This selection applies only to that repo, so another app can follow stable or select a different version at the same time. Remove the field to follow stable again.

For a one-off module swap without writing a config file, pass it inline:

terminal

sootsim open <port> --replace react-native-camera=./stubs/camera.ts

sootsim maestro test accepts the same --replace <module>=<file> flag, so a test run can stub a module for just that run.

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-16, …, iphone-17-pro-maxiphone-17-pro
orientation--orientationportrait, landscapeportrait

Appearance

settingCLI flagvaluesdefault
colorScheme--themelight, dark, autoauto
reduceMotionnonebooleanfalse
boldTextnonebooleanfalse
fontSizenone0.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
showTouchesnonebooleanfalse
showStatusBarnonebooleantrue
showHomeIndicatornonebooleantrue
a11yModenoneoff, delayed, activedelayed
inspectModenonebooleanfalse

Optional dev-server integration

If you want a stable /__contrast 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, so apply module overrides with --replace (see “How the config reaches the runtime” above) when driving through a plugin.

Ready to build?

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

curl -fsSL https://sootsim.com/install.sh | sh