name: sdk-react-native description: Build, debug, and maintain the Onde Inference React Native / Expo SDK. Covers Expo native module wiring, Metro config for file-linked SDKs, iOS deployment target and Expo autolinking, XCFramework device/simulator split, app group entitlement, HF_HOME/TMPDIR sandbox setup, the native bridge JSON string contract, EngineStatus normalization, and npm publishing. allowed-tools: Read, Write, Edit, Glob, Grep, Bash

user-invocable: true

Skill: React Native / Expo SDK

What This Skill Covers

Building and maintaining the @ondeinference/react-native Expo native module, wiring the Rust C FFI bridge to TypeScript, debugging Metro/CocoaPods/Xcode issues, and publishing to npm.


Repository Layout

onde/sdk/react-native/
├── package.json                   # name: @ondeinference/react-native
├── expo-module.config.json        # declares OndeInferenceModule for Expo autolinking
├── tsconfig.json                  # target ES2020, module commonjs
├── src/
│   ├── index.ts                   # public API: OndeChatEngine, free functions, types
│   ├── OndeInferenceModule.ts     # requireNativeModule("OndeInference") — raw bridge
│   └── types.ts                   # ChatMessage, SamplingConfig, EngineInfo, etc.
├── build/                         # tsc output — gitignored, regenerated by npm run build
├── ios/
│   ├── OndeInference.podspec      # CocoaPods spec — vendored_frameworks = XCFramework
│   ├── OndeInferenceModule.swift  # Expo Module: Rust C FFI declarations + module def
│   └── rust/
│       ├── OndeReactNative.xcframework  # bundles device + simulator slices
│       ├── device/libonde_react_native.a
│       └── simulator/libonde_react_native.a
├── android/                       # Android module (not yet wired for Onde)
├── scripts/
│   └── build-rust.sh              # builds Rust .a for iOS device + sim, creates XCFramework
└── example/
    ├── app.json                   # Expo config — ios.deploymentTarget, appleTeamId
    ├── babel.config.js            # babel-preset-expo only
    ├── metro.config.js            # watchFolders, extraNodeModules, nodeModulesPaths, blockList
    ├── package.json               # dep: "@ondeinference/react-native": "file:.."
    └── ios/
        ├── Podfile                # uses use_expo_modules!, deployment from Podfile.properties.json
        ├── Podfile.properties.json   # ios.deploymentTarget: "16.0" — critical, see below
        ├── OndeExample/
        │   ├── OndeExample.entitlements
        │   └── Info.plist
        └── Pods/                  # gitignored

Critical Lessons (Read First)

1. Metro @babel/runtime resolution with file:.. local packages

Symptom: Unable to resolve module @babel/runtime/helpers/asyncToGenerator from inside the SDK's build/index.js.

Root cause: When Metro processes a file physically located outside the example app's directory (e.g. in sdk/react-native/build/), Babel transforms async/await and injects require("@babel/runtime/..."). Metro resolves modules relative to the file's physical location — but the SDK's own node_modules/ is blocked (to avoid react-native version conflicts) and the example's node_modules/ is not in the resolution path for files outside the example root.

Fix — always required in metro.config.js:

// Falls back to the example's node_modules for any file in watched folders
config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")];

Also required: block the SDK's node_modules to avoid react-native version conflicts:

config.resolver.blockList = [
  new RegExp(
    path.resolve(sdkRoot, "node_modules").replace(/\\/g, "\\\\") + "/.*",
  ),
];

2. Expo autolinking silently skips pods when deployment target is too low

Symptom: pod install completes but OndeInference is not in Podfile.lock. The app launches but throws "Cannot find native module 'OndeInference'" at runtime.

Root cause: Expo's autolinking Ruby code calls pod.supports_platform? on each discovered pod. The OndeInference podspec declares s.platforms = { :ios => '16.0' }. If the project's deployment target (from Podfile.properties.json) is lower (e.g. the Expo default of 15.1), supports_platform? returns false and the pod is silently skipped with no error or warning.

Diagnosis:

xcrun simctl get_app_container booted <bundle-id> data
grep "OndeInference" ios/Podfile.lock   # if empty → autolinking skipped it

Fix — three places must all agree on iOS 16.0:

  1. sdk/react-native/ios/OndeInference.podspec — s.platforms = { :ios => '16.0' }
  2. example/ios/Podfile.properties.json — "ios.deploymentTarget": "16.0"
  3. example/app.json → expo.ios.deploymentTarget: "16.0"

And after changing Podfile.properties.json, manually patch the Xcode project:

sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = 15.1/IPHONEOS_DEPLOYMENT_TARGET = 16.0/g' \
    ios/OndeExample.xcodeproj/project.pbxproj

Then run pod install again and verify:

grep "OndeInference" ios/Podfile.lock   # must now appear

Important: expo prebuild --clean regenerates Podfile.properties.json from app.json. After --clean, you must also manually re-add "ios.deploymentTarget": "16.0" to Podfile.properties.json if it was not picked up from app.json.


3. iOS device vs simulator static library — use an XCFramework

Symptom: Build succeeds for simulator but fails with: ld: building for 'iOS-simulator', but linking in object file built for 'iOS'

Root cause: Both iOS device (aarch64-apple-ios) and iOS simulator (aarch64-apple-ios-sim) are arm64, so lipo cannot merge them into a fat binary. They have different Mach-O platform metadata.

Wrong approach: Single libonde_react_native.a → works for device OR simulator but not both.

Wrong approach: vendored_libraries with two files sharing the same filename → CocoaPods reports "conflicting names" error.

Correct approach: XCFramework bundling both slices:

# In scripts/build-rust.sh
mkdir -p ios/rust/device ios/rust/simulator
cp target/aarch64-apple-ios/release/libonde_react_native.a         ios/rust/device/
cp target/aarch64-apple-ios-sim/release/libonde_react_native.a     ios/rust/simulator/

rm -rf ios/rust/OndeReactNative.xcframework
xcodebuild -create-xcframework \
    -library ios/rust/device/libonde_react_native.a \
    -library ios/rust/simulator/libonde_react_native.a \
    -output ios/rust/OndeReactNative.xcframework

Podspec uses vendored_frameworks (not vendored_libraries):

s.vendored_frameworks = 'rust/OndeReactNative.xcframework'
s.pod_target_xcconfig = {
  'DEFINES_MODULE' => 'YES',
  'SWIFT_COMPILATION_MODE' => 'wholemodule',
  'OTHER_LDFLAGS' => '-londe_react_native',
}

CocoaPods automatically selects the correct slice for device vs simulator.


4. The native bridge JSON string contract — never return Swift dicts

Symptom: JSON parse error: unexpected character: o

Root cause: The TypeScript layer receives the native return value and calls JSON.parse(value). If the native Swift module returns [String: Any] or [[String: Any]] instead of a String, the Expo bridge serialises it in an opaque way that is not valid JSON (the string representation starts with something like {...} or [...] in a non-JSON form that starts with o when printed).

The contract: All Expo Module methods in OndeInferenceModule.swift that return complex types MUST return String (the raw JSON from Rust), NOT [String: Any] or [[String: Any]]. The TypeScript layer owns all JSON parsing.

Correct Swift:

AsyncFunction("loadDefaultModel") {
    (systemPrompt: String?, samplingJson: String?) -> String in  // ← String, not [String: Any]
    ...
    return try self.validateJsonResult(result)  // returns the raw JSON string
}

Function("defaultModelConfig") { () -> String in  // ← String, not [String: Any]
    let result = self.consumeRustString(onde_default_model_config())
    return try self.validateJsonResult(result)
}

Validation helper — validate but do not deserialise:

private func validateJsonResult(_ json: String?) throws -> String {
    guard let json = json,
        let data = json.data(using: .utf8),
        let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any]
    else { throw OndeError.invalidResponse }
    if let error = dict["error"] as? String {
        throw OndeError.inferenceError(error)
    }
    return json  // ← return the original string, not the parsed dict
}

private func validateJsonArray(_ json: String?) throws -> String {
    guard let json = json,
        let data = json.data(using: .utf8),
        (try JSONSerialization.jsonObject(with: data) as? [[String: Any]]) != nil
    else { throw OndeError.invalidResponse }
    return json  // ← return the original string
}

5. EngineStatus is lowercase from Rust — normalize in the UI layer

Symptom: Model loads successfully. Alert says "Ready in X.Xs". But the TextInput placeholder stays at "Load a model first" and remains non-editable.

Root cause: Rust's Display impl emits lowercase status strings: "ready", "unloaded", "loading", "generating", "error". The TypeScript UI uses capitalized union type values ("Ready", "Unloaded", etc.). After refreshStatus() runs, engineStatus holds "ready" which does not match "Ready" in the isLoaded check.

Fix — normalize at the point of receiving native status:

const normalizeEngineStatus = useCallback((status: string): EngineStatus => {
  switch (status.toLowerCase()) {
    case "loading":    return "Loading";
    case "ready":      return "Ready";
    case "generating": return "Generating";
    case "error":      return "Error";
    default:           return "Unloaded";
  }
}, []);

const refreshStatus = useCallback(async () => {
  try {
    const info = await OndeChatEngine.info();
    setEngineStatus(normalizeEngineStatus(info.status));  // ← normalize here
    setModelName(info.modelName ?? null);
    setApproxMemory(info.approxMemory ?? null);
  } catch { /* best-effort */ }
}, [normalizeEngineStatus]);

6. iOS sandbox filesystem setup — HF_HOME must be set before model load

Symptom: Model load fails immediately with "error 0" or returns an error containing "HF_HOME is not set".

Root cause: On iOS, ~/.cache is outside the app sandbox. The Rust engine explicitly requires HF_HOME to be set before any model load on sandboxed platforms. Without it, load_gguf_model returns an InferenceError::ModelBuild with the message: "HF_HOME is not set — cannot initialise HF cache."

Fix — call configureApplicationFilesystem() in OnCreate and before each model load:

private func configureApplicationFilesystem() throws {
    let fileManager = FileManager.default

    let containerDirectory: URL
    if let appGroupDirectory = fileManager.containerURL(
        forSecurityApplicationGroupIdentifier: ondeAppGroupIdentifier
    ) {
        containerDirectory = appGroupDirectory
    } else {
        containerDirectory = try fileManager.url(
            for: .applicationSupportDirectory,
            in: .userDomainMask, appropriateFor: nil, create: true
        )
    }

    // Convention: <container>/models/hub — matches Tauri and Swift SDKs exactly
    let hfHomeDirectory = containerDirectory.appendingPathComponent("models", isDirectory: true)
    let hfHubCacheDirectory = hfHomeDirectory.appendingPathComponent("hub", isDirectory: true)
    let tempDirectory = containerDirectory.appendingPathComponent("tmp", isDirectory: true)

    try fileManager.createDirectory(at: hfHomeDirectory,    withIntermediateDirectories: true)
    try fileManager.createDirectory(at: hfHubCacheDirectory, withIntermediateDirectories: true)
    try fileManager.createDirectory(at: tempDirectory,       withIntermediateDirectories: true)

    setenv("HF_HOME",               hfHomeDirectory.path,    1)
    setenv("HF_HUB_CACHE",          hfHubCacheDirectory.path, 1)
    setenv("HUGGINGFACE_HUB_CACHE", hfHubCacheDirectory.path, 1)
    setenv("TMPDIR",                tempDirectory.path,       1)
}

Call it in OnCreate and at the start of loadDefaultModel/loadModel:

OnCreate {
    do {
        try self.configureApplicationFilesystem()
        self.enginePtr = onde_engine_create()
    } catch {
        self.enginePtr = nil
    }
}

AsyncFunction("loadDefaultModel") { ... -> String in
    try self.configureApplicationFilesystem()  // ← also here, idempotent
    guard let ptr = self.enginePtr else { throw OndeError.engineNotInitialized }
    ...
}

7. Shared app group cache path convention

All Onde-powered apps across all SDKs (Tauri, Swift, React Native, Flutter) share the same HuggingFace cache via the App Group container. The layout is:

<group.com.ondeinference.apps container>/
├── models/           ← HF_HOME
│   └── hub/          ← HF_HUB_CACHE  (GGUF files live here)
└── tmp/              ← TMPDIR

The subdirectory is models/, NOT huggingface/.

Do NOT use OndeInference/huggingface/hub/ — this was an early mistake that caused the app to look in a different place than the Tauri SDK, preventing cross-app model reuse.

Verifying the shared container is accessible on simulator:

xcrun simctl get_app_container booted <bundle-id> groups
# → shows group.com.ondeinference.apps path

xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps
# → resolves the exact path

# Check whether models exist in the shared cache
find "$(xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps)/models/hub" \
    -name "*.gguf" -o -name "*.part" -o -name "*.lock" | sort

8. Entitlements required for iOS

The example app's OndeExample.entitlements must contain:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.developer.kernel.increased-memory-limit</key>
    <true />
    <key>com.apple.security.application-groups</key>
    <array>
      <string>group.com.ondeinference.apps</string>
    </array>
  </dict>
</plist>
  • increased-memory-limit — required for ~1 GB+ model loading on iOS
  • com.apple.security.application-groups — grants access to the shared model cache

On physical device: the App Group must be registered in the Apple Developer Portal under Identifiers → App Groups before Xcode can provision it.

On the iOS Simulator the entitlement is granted automatically by the simulator runtime — no provisioning profile needed.


9. FlatList swallows taps — TextInput never gets focus

Symptom: After model loads, the TextInput placeholder is correct but tapping it does nothing. The keyboard never appears.

Root cause: FlatList on iOS intercepts touch events and by default dismisses the keyboard on scroll, which prevents child inputs from receiving focus when tapping in certain layout configurations.

Fix:

<FlatList
  ...
  keyboardShouldPersistTaps="handled"   // ← prevents FlatList eating taps
/>

10. Do not use the iOS Simulator to validate model loading

The iOS Simulator is unreliable for validating actual LLM model loading with this stack: - Metal-backed inference behaves differently in the simulator - Large GGUF model loading (~941 MB) may hang or fail in the simulator - Shared app group container behavior can differ

Use the Simulator for: - Build verification - UI layout and interaction - Entitlement/container path checks (xcrun simctl get_app_container ...)

Use a physical iPhone running iOS 16+ for: - Actual model download and load - Metal inference correctness - Memory behavior


11. Diagnosing a hung model load

When the app stays in "Loading" state indefinitely:

  1. Check the shared container has no stale partial downloads:
# Find .part and .lock files
find "$(xcrun simctl get_app_container booted <bundle-id> group.com.ondeinference.apps)" \
    -type f \( -name "*.part" -o -name "*.lock" \) | sort

# Remove stale partial caches
rm -rf "<container>/models/hub/models--<org>--<model>"
  1. Check the app group resolves:
xcrun simctl get_app_container booted <bundle-id> groups
  1. Check native logs:
xcrun simctl spawn booted log show --style compact --last 5m \
    --predicate 'process == "OndeExample" AND (
        eventMessage CONTAINS[c] "HF_" OR
        eventMessage CONTAINS[c] "models" OR
        eventMessage CONTAINS[c] "onde"
    )' | tail -100
  1. Verify HF_TOKEN was baked in at Rust build time. On iOS, TokenSource::CacheToken cannot read ~/.cache/huggingface/token because that path is outside the sandbox. HF_TOKEN must be set as an environment variable when building the Rust static library:
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
./scripts/build-rust.sh ios

option_env!("HF_TOKEN") in src/inference/token.rs bakes it into the binary at compile time. Setting it after the fact has no effect.


Architecture

┌──────────────────────────────────────────────────────┐
│  React Native / TypeScript                           │
│  OndeChatEngine, free functions (index.ts)           │
└─────────────────────┬────────────────────────────────┘
                      │ JSON strings over Expo bridge
                      ▼
┌──────────────────────────────────────────────────────┐
│  OndeInferenceModule.swift (Expo Module)             │
│  - configureApplicationFilesystem()                  │
│  - validateJsonResult() / validateJsonArray()        │
│  - AsyncFunction / Function definitions              │
└─────────────────────┬────────────────────────────────┘
                      │ C FFI via @_silgen_name
                      ▼
┌──────────────────────────────────────────────────────┐
│  libonde_react_native.a (Rust static lib)            │
│  C API: onde_engine_create, onde_engine_load_*, ...  │
│  Rust engine: ChatEngine, mistral.rs, Metal/CPU      │
└──────────────────────────────────────────────────────┘

The Swift module does not use UniFFI. It calls the Rust C API directly via @_silgen_name declarations, communicating via JSON strings over the Expo Module bridge.

This is different from the Swift XCFramework SDK (which uses UniFFI-generated onde.swift) and the Kotlin SDK (which uses UniFFI-generated onde.kt).


Expo Module Definition Reference

All methods in OndeInferenceModule.swift follow this pattern:

Expo construct Use for
Function(name) Synchronous calls (isLoaded, setSystemPrompt, etc.)
AsyncFunction(name) All calls that invoke Rust (model load, inference, etc.)
OnCreate Engine initialisation — run configureApplicationFilesystem() here
OnDestroy Engine cleanup — call onde_engine_destroy

Return types: - Simple sync functions: Bool, Int - All complex types: String — raw JSON from Rust, never [String: Any]


TypeScript Bridge Contract

OndeInferenceModule.ts — raw native interface

All async methods return Promise<string> (raw JSON strings from Rust):

export interface OndeInferenceNativeModule {
  // Sync
  isLoaded(): boolean;
  setSystemPrompt(prompt: string): void;
  clearHistory(): number;

  // Async — all return JSON strings
  loadDefaultModel(systemPrompt: string | null, samplingJson: string | null): Promise<string>;
  sendMessage(message: string): Promise<string>;
  info(): Promise<string>;
  history(): Promise<string>;

  // Sync config — return JSON strings
  defaultModelConfig(): string;
  defaultSamplingConfig(): string;
}

index.ts — public API layer

Serialises inputs to JSON, calls native, deserialises JSON responses:

// Input serialisation (camelCase TS → snake_case Rust)
function serializeSampling(config: SamplingConfig): string {
  return JSON.stringify({ temperature: config.temperature, top_p: config.topP, ... });
}

// Output deserialisation (snake_case Rust → camelCase TS)
function parseResult<T>(json: string): T {
  const parsed = JSON.parse(json);       // JSON.parse — expects a string
  if (parsed.error) throw new OndeError(parsed.error);
  return parsed as T;
}

Metro Config for file:.. SDK Development

const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");

const sdkRoot = path.resolve(__dirname, "..");  // ../sdk/react-native/
const config = getDefaultConfig(__dirname);

// 1. Watch the SDK so Metro picks up changes
config.watchFolders = [sdkRoot];

// 2. Map the package name to the SDK root
config.resolver.extraNodeModules = {
  "@ondeinference/react-native": sdkRoot,
};

// 3. Allow transitive deps (e.g. @babel/runtime) to resolve from
//    the example's node_modules when files outside the example are processed
config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")];

// 4. Block the SDK's own node_modules to prevent react-native version conflicts
config.resolver.blockList = [
  new RegExp(
    path.resolve(sdkRoot, "node_modules").replace(/\\/g, "\\\\") + "/.*",
  ),
];

module.exports = config;

Rust C API Surface

The Swift module binds to these C functions via @_silgen_name:

Function Returns Notes
onde_engine_create UnsafeMutableRawPointer? Allocates engine
onde_engine_destroy void Frees engine
onde_engine_load_default_model UnsafeMutablePointer<CChar>? JSON result string
onde_engine_load_model UnsafeMutablePointer<CChar>? JSON result string
onde_engine_unload_model UnsafeMutablePointer<CChar>? JSON result string
onde_engine_is_loaded Bool Sync
onde_engine_info UnsafeMutablePointer<CChar>? JSON result string
onde_engine_send_message UnsafeMutablePointer<CChar>? JSON result string
onde_engine_generate UnsafeMutablePointer<CChar>? JSON result string
onde_engine_history UnsafeMutablePointer<CChar>? JSON array string
onde_engine_clear_history UInt64 Sync, count
onde_engine_set_system_prompt void Sync
onde_engine_set_sampling void Sync
onde_engine_push_history void Sync
onde_free_string void Must call for every non-null CChar* result
onde_default_model_config UnsafeMutablePointer<CChar>? JSON result string
onde_qwen25_1_5b_config UnsafeMutablePointer<CChar>? JSON result string
onde_qwen25_3b_config UnsafeMutablePointer<CChar>? JSON result string
onde_default_sampling_config UnsafeMutablePointer<CChar>? JSON result string
onde_deterministic_sampling_config UnsafeMutablePointer<CChar>? JSON result string
onde_mobile_sampling_config UnsafeMutablePointer<CChar>? JSON result string

Always call onde_free_string after consuming each result pointer. The Rust side allocates the string; Swift is responsible for freeing it.

private func consumeRustString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
    guard let ptr = ptr else { return nil }
    let str = String(cString: ptr)
    onde_free_string(ptr)   // ← must free every non-null pointer
    return str
}

build-rust.sh — iOS Build Flow

# 1. Build both targets
cargo build --manifest-path rust/Cargo.toml --target aarch64-apple-ios --release
cargo build --manifest-path rust/Cargo.toml --target aarch64-apple-ios-sim --release

# 2. Copy to separate dirs (same filename — cannot lipo)
mkdir -p ios/rust/device ios/rust/simulator
cp target/aarch64-apple-ios/release/libonde_react_native.a         ios/rust/device/
cp target/aarch64-apple-ios-sim/release/libonde_react_native.a     ios/rust/simulator/

# 3. Create XCFramework (CocoaPods picks right slice automatically)
rm -rf ios/rust/OndeReactNative.xcframework
xcodebuild -create-xcframework \
    -library ios/rust/device/libonde_react_native.a \
    -library ios/rust/simulator/libonde_react_native.a \
    -output ios/rust/OndeReactNative.xcframework

After rebuilding, run pod install in example/ios/ and then npx expo run:ios --device or npx expo run:ios (simulator).


Running on Physical Device

# From example/
npx expo run:ios --device

For a physical device build you need: - Apple Developer team set in app.json → expo.ios.appleTeamId - The App Group group.com.ondeinference.apps registered in the developer portal - The App Group capability enabled on the App ID - com.apple.developer.kernel.increased-memory-limit in entitlements

Blank white screen on device = Metro bundle not loading. Ensure: 1. Metro is running (npx expo start --clear) 2. Phone and Mac are on the same network (or use Tunnel) 3. Rebuild after native changes (npx expo run:ios --device — not just expo start)


.gitignore for the SDK

node_modules/
build/                          # tsc output
rust/target/                    # Rust build artifacts
ios/build/
ios/rust/*.a
ios/rust/*.xcframework
ios/rust/device/
ios/rust/simulator/
android/build/
android/src/main/jniLibs/
example/.expo/
example/android/
example/ios/build/
example/ios/Pods/
example/ios/.xcode.env.local
.DS_Store
*.xcuserstate

npm Publishing

cd sdk/react-native
npm run build          # tsc → build/
npm publish            # publishes build/, ios/, android/, scripts/

The .npmignore excludes: - example/ - rust/ - node_modules/ - src/ (consumers use the compiled build/)

The package.json files field explicitly includes: build/, ios/, android/, scripts/, expo-module.config.json


Common Pitfalls Table

Pitfall Symptom Fix
Missing nodeModulesPaths in metro.config.js Cannot resolve @babel/runtime/helpers/asyncToGenerator Add config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")]
Deployment target mismatch Pod installs but OndeInference missing from Podfile.lock Set "ios.deploymentTarget": "16.0" in Podfile.properties.json AND app.json
Single .a file for device + simulator Linker error: "building for iOS-simulator but linked for iOS" Use xcodebuild -create-xcframework to bundle both slices
Two .a files with same name in vendored_libraries CocoaPods "conflicting names" error Use vendored_frameworks with an XCFramework instead
Swift returning [String: Any] from Expo Module JSON parse error: unexpected character: o Return String from all Expo Module functions; validate but do not deserialise
HF_HOME not set on iOS "error 0" or model load fails immediately Call configureApplicationFilesystem() in OnCreate and before model load
Wrong cache path (OndeInference/huggingface/hub/) Models not shared across Onde apps Use <container>/models/hub/ to match Tauri/Swift convention
EngineStatus lowercase from Rust Input stays disabled after model loads; status shows as "ready" not "Ready" Normalize in refreshStatus() with switch(status.toLowerCase())
FlatList swallows taps TextInput can't be focused Add keyboardShouldPersistTaps="handled" to FlatList
Model hangs on Simulator Stuck in loading indefinitely Test model loading on a physical device; Simulator is unreliable for this
Stale .part/.lock files Model download never completes on retry Delete models--<org>--<model> from the shared container and retry
HF_TOKEN not baked in Auth fails on HuggingFace download; "401 Unauthorized" in Rust logs Set HF_TOKEN env var before building the Rust static library
expo prebuild --clean wipes Podfile.properties.json Deployment target reverts to 15.1; autolinking drops OndeInference Re-add "ios.deploymentTarget": "16.0" to Podfile.properties.json after every --clean
Physical device blank white screen App launches, nothing renders Ensure Metro is running and on same network; use npx expo run:ios --device
Running with expo start instead of expo run:ios "Cannot find native module 'OndeInference'" Custom native modules require a native build — use expo run:ios not Expo Go