◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/AI Models
◉ AI Models/2026-06-14Advanced

Calling Apple Foundation Models from a Rork (Expo) App: Bridging On-Device AI Through a Native Module

Rork generates Expo (React Native) apps, but Apple Foundation Models ships as a Swift framework you can't touch from JavaScript. Here's how to write an Expo Modules API bridge, gate it by availability, and fall back to the cloud on unsupported devices.

Rork611Expo234Foundation Models4On-Device AI6Native ModuleReact Native242

✦ Premium Article

Generate an app from a prompt in Rork and what you get is an Expo (React Native) project. Apple's on-device AI, Foundation Models, ships as a Swift framework. So the moment you decide you want an on-device LLM inside a Rork-built app, you hit a wall: JavaScript can't reach it directly.

I've run wallpaper and wellness apps as an indie developer for years, and one thing has been hammered into me repeatedly: "shipping a new OS feature to every user" and "running it on the latest devices only" are two completely different things. Foundation Models is an iOS 26+ on-device capability, so calling it naively quietly abandons readers on older hardware. In this article we build a production-grade setup that bridges Swift's Foundation Models through the Expo Modules API and silently routes to the cloud when the environment can't support it.

The design philosophy of using on-device inference as the primary path and escaping heavy work to the cloud is covered in the on-device-first inference router, and the prerequisite of using Foundation Models from native Swift is covered in the Apple FoundationModels implementation guide. This piece narrows in on the wiring that sits between those two: how you actually call that framework from an Expo app.

Why JavaScript Can't Call It Directly

Foundation Models is a Swift-only API you reach via import FoundationModels. The React Native bridge only passes JSON-equivalent values between JavaScript and Objective-C/Swift, so a Swift type like LanguageModelSession can't cross over to JS as-is.

This is where many people go hunting for an off-the-shelf package like expo-apple-intelligence. But Foundation Models is recent, and thin wrappers aimed purely at Expo apps aren't yet stable. I tend to decide that rather than being dragged around by someone else's black-box wrapper and its version churn, I'd rather write and own a hundred-line module myself — and I'll take that approach here too. There are only three things to do: receive a prompt as a string, call Foundation Models on the Swift side, and return the result string through a Promise. For that scope, a hand-written module reads more clearly and you can fix it yourself when it breaks.

The Swift Side: Wrapping Foundation Models in an Expo Module

In the Expo Modules API, you just define AsyncFunctions on a class that extends Module, and you get functions JS can await. To build a local module, start from npx create-expo-module --local on-device-ai, then rewrite the generated ios/OnDeviceAIModule.swift like this.

import ExpoModulesCore
import FoundationModels
 
public class OnDeviceAIModule: Module {
  public func definition() -> ModuleDefinition {
    Name("OnDeviceAI")
 
    // Tell JS whether this device can run the on-device LLM.
    // Anything other than "available" means JS should switch to the cloud.
    AsyncFunction("availability") { () -> String in
      if #available(iOS 26.0, *) {
        switch SystemLanguageModel.default.availability {
        case .available:
          return "available"
        case .unavailable(.deviceNotEligible):
          return "device_not_eligible"
        case .unavailable(.appleIntelligenceNotEnabled):
          return "not_enabled"
        case .unavailable(.modelNotReady):
          return "model_not_ready"
        case .unavailable:
          return "unavailable"
        }
      } else {
        return "os_too_old"   // below iOS 26
      }
    }
 
    // Take a prompt, return generated text.
    // Using a promise bridges Swift's async/throws straight into a JS exception.
    AsyncFunction("generate") { (prompt: String, promise: Promise) in
      guard #available(iOS 26.0, *) else {
        promise.reject("UNSUPPORTED", "On-device generation is unavailable below iOS 26")
        return
      }
      Task {
        do {
          let session = LanguageModelSession()
          let response = try await session.respond(to: prompt)
          promise.resolve(response.content)
        } catch {
          promise.reject("GENERATION_FAILED", error.localizedDescription)
        }
      }
    }
  }
}

The single most important thing here is the if #available(iOS 26.0, *) guard. Even in code that imports FoundationModels, as long as you keep Foundation Models-specific types out of anything outside the guard, the framework is weak-linked and the app still launches on pre-iOS-26 devices. I keep availability as its own function and guard again at the top of generate because that lets the JS side survive both "branch on availability before calling" and "accidentally call generate on an old device." In production the latter always happens eventually, so I defend twice.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦If you were stuck wanting on-device AI in your Expo app but couldn't reach the Swift framework from JS, you'll now have a working bridge you wrote and control yourself
✦You can turn a naive call that crashes on pre-iOS-26 or ineligible devices into something you can ship to every user, guarded by an availability check and a cloud fallback
✦You'll be able to weak-link the system framework through EAS Build and decide, with a real sense of device distribution, how to pass review and staged rollout
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◉ AI Models2026-08-07
Cutting MCP Tools Didn't Make Anything Lighter — 2,377 Bytes of Definitions vs 110,298 Bytes of Response
I wrote a minimal MCP server for a wallpaper catalog and measured the byte cost of tool definitions against the byte cost of responses. Here is which side actually matters, and the real reason to merge tools.
◉ AI Models2026-10-25
Is Your Rork App Still Calling Sonnet 4.5? Finding Out Before November 30
Claude Sonnet 4.5 is scheduled to retire from the API on November 30, 2026. A small script to find leftover model IDs in your app, and the order to fix them in depending on where they live.
◉ AI Models2026-09-12
Ask Rork for an iOS 27 feature and you may get last year's code back, with no error at all
iOS 27 ships on September 14. When a model is asked for an API it has never seen, there are two kinds of failure: the kind that stops your build, and the kind that does not. Here is why standard Rork and Rork Max differ, and the order I check generated code in.