Keel Prebuild
keel prebuild <platform> is the Expo-prebuild equivalent: it turns a
managed Keel JS project (no android//ios/ folder) into a compilable
native project you build locally with the platform toolchain. It is the
local counterpart of cloud dev-client — the developer’s
own machine is the build host, so no macOS/Android-SDK runner infrastructure is
needed on our side. Both platforms are verified end-to-end (the built app
mounts its embedded bundle and renders on a device/simulator).
# Android → sideloadable APK
keel prebuild android # generate ./android from app.json + package.json
cd android && ./gradlew assembleRelease
# → app/build/outputs/apk/release/app-release.apk (sideload-ready, debug-signed)
# iOS → .app (macOS + Xcode). prebuild runs xcodegen + pod install for you.
keel prebuild ios
cd ios && xcodebuild -workspace <Name>.xcworkspace -scheme <Name> \
-destination 'platform=iOS Simulator,name=iPhone 16' build
The produced app embeds the project’s JS (assets/index.android.bundle as
Hermes bytecode on Android; main.jsbundle on iOS) and mounts it through
KeelRuntimeHost — no dev server, no network. It is a standalone app with its
own applicationId/bundleId + label, not the shared Keel Go tester.
app.json — the identity config
Prebuild reads app identity from app.json (Expo-parallel; keel.json stays
build/OTA config). Only android.package is really needed:
{
"name": "My App", // launcher label (falls back to slug)
"slug": "my-app", // falls back to package.json name
"version": "1.0.0", // versionName (falls back to package.json version)
"android": {
"package": "com.example.myapp", // applicationId — derived if absent (with a warn)
"versionCode": 1
},
"ios": {
"bundleIdentifier": "com.example.myapp",
"appleTeamId": "AB12CD34EF", // Apple Developer Team ID — enables signed .ipa
"distribution": "development" // development | ad-hoc | app-store
}
}
If android.package is missing, prebuild derives com.keelapp.<slug> and warns
— pin it, the applicationId is your app’s permanent identity.
iOS signing → .ipa → store
Set ios.appleTeamId (your Apple Developer Team ID) and keel prebuild ios wires
automatic signing into the Xcode project + emits an ios/ExportOptions.plist.
Then the archive/export produces a signed .ipa (Xcode resolves your cert +
provisioning for that team):
keel prebuild ios # generates signed project + ExportOptions.plist
cd ios
xcodebuild -workspace <Name>.xcworkspace -scheme <Name> \
-destination 'generic/platform=iOS' -archivePath build/<Name>.xcarchive archive
xcodebuild -exportArchive -archivePath build/<Name>.xcarchive \
-exportOptionsPlist ExportOptions.plist -exportPath build/ipa
# → build/ipa/<Name>.ipa
keel submit ios --build build/ipa/<Name>.ipa --to ios_appstore \
--app-id <bundle> --version 1.0.0 --build-number 1 --credentials creds.yml
keel submit ios (the appstore driver) mints an App Store Connect JWT from your
.p8 key, uploads via xcrun altool, waits for the build to go VALID, and files a
TestFlight review submission. All signing/upload credentials are yours (cert,
provisioning, ASC API key) — prebuild + submit are the pipeline around them. Without
ios.appleTeamId, prebuild builds a simulator-only (unsigned) .app.
How it works
Prebuild copies a parameterized template of the Keel Go Android host (which is
not a standard RN ReactNativeHost app — modules register through the global
KeelModuleRegistrar, and the bundle mounts through KeelRuntimeHost), baking
in absolute toolchain paths so the project builds wherever node_modules
is hoisted:
- Resolves the
node_modulesroot (walks up; handles workspace hoisting). - Generates
android/with the app’sapplicationId/ label / version, entry pointed at the project’sindex.tsx. - Wires Keel’s own autolinker — gradle runs
keel-module autolinkat build time to generateKeelModulesProvider.kt+ the moduleinclude/depsscripts, sonpm add @keel-ai/fooneeds no host edits. - Writes
react-native.config.jsopting the Keel modules out of RN community autolinking (Keel links them itself).
The native foundation comes from the prebuilt com.appunvs:keel-module-core
AAR (maven); the KeelView Fabric component compiles in-situ from the
@keel-ai/modules-core npm package’s cpp/.
Requirements
Checked by keel doctor. Android: JDK 17, Android SDK + NDK 26.x, and
com.appunvs:keel-module-core resolvable from maven (mavenLocal during
development). iOS (macOS only): Xcode 26+, xcodegen, CocoaPods, and
KeelModuleCore.xcframework under keel/build/ios/ (SPM .binaryTarget).
The project needs these devDeps — RN 0.85 unbundled them:
@react-native-community/cli— metrobundlerefuses to run without it.hermes-compiler— shipshermesc(Android); prebuild pointsreact.hermesCommandatnode_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc.
What prebuild generates
- Android:
android/(gradle project) +react-native.config.js. You run./gradlew assembleRelease. - iOS:
ios/(XcodeGenproject.yml+Podfile+<Name>/MainApp.swift+ embeddedmain.jsbundle+ autolinked Swift providers), and prebuild runsxcodegen generate+pod installfor you (likeexpo prebuild). You runxcodebuild … build— with a concrete-destination, not-sdk(-sdk iphonesimulatorforces the@KeelModulemacro plugin onto the simulator SDK → Pitfall 17).
v1 limitations
- iOS is debug-signed for the simulator / local device. Store / TestFlight
distribution needs a real signing identity + provisioning profile (that’s
keel submit’s input) and, for a physical device, an Apple Developer team. - Android release is debug-signed so
assembleReleaseproduces a sideloadable APK out of the box. PointsigningConfigs { create("release") }at a real keystore to ship. android//ios/are build artifacts — regenerate with--clean, don’t hand-edit.
Community RN native modules
Both platforms autolink genuine community RN native modules (react-native-screens,
react-native-safe-area-context, …) alongside Keel’s own @keel-ai/* /
keel-module-* modules — install the module (npm add), re-run prebuild, done.
Two autolinkers run in parallel (this is how Expo does it): Keel’s
keel-module autolink for Keel modules, and RN’s community autolinker for the
rest (react-native.config.js marks the Keel modules android:null so they
aren’t double-linked).
- Android wires RN community autolinking via
com.facebook.react.settings+react.autolinkLibrariesWithApp();MainActivityregisters the community Java ViewManagers throughKeelRuntimeHost.additionalPackages = PackageList(...). Needs NDK 27.1 (RN 0.85’s prefab Fabric headers use C++20std::format, which NDK 26’s libc++ gates) and the RN-standardrootProject.ext.compileSdkVersion— both are baked into the generated project. - iOS gets community modules free via the Podfile’s
use_native_modules!.
Notes for maintainers
Bringing up prebuild surfaced gaps in the release-build + community-autolink paths
that Keel Go’s Android project shares but had never exercised (Keel Go was only
ever built assembleDebug, never a clean release, and opts community modules out):
autolinking.json— Keel Go passed only on a stale checked-inbuild/copy. Prebuild now appliescom.facebook.react.settings+autolinkLibrariesFromCommand(the “JsonUtils NoClassDefFound” fear was stale).- RN 0.85 moved
hermescinto thehermes-compilerpackage; fixed viareact.hermesCommand. - R8 strips
kotlin.jvm.functions.Function1.invoke(libkeel.so’sJNI_OnLoadresolves it by name →NoSuchMethodErrorabort). Fixed with keep rules; these now ship inkeel-module-core.aar(consumer-rules.promoved intodefaultConfig— the publish is the DEBUG variant, so it can’t sit inbuildTypes.release). App-level keeps kept as belt-and-suspenders. @react-native-community/climust be a project devDep (RN 0.85 unbundled it).- Community Fabric modules need NDK 27.1 (RN prefab
std::format),rootProject.ext.compileSdkVersion, and — for Java ViewManagers —KeelRuntimeHost.additionalPackages = PackageList(...).
A clean assembleRelease of keel/go/android would hit (1)–(3) as well.