Keel — Architecture
Keel splits like Expo into two tiers: the framework (SDK + CLI) and the cloud tier — KAS (Keel Application Services), the analog of Expo’s EAS. KAS is the umbrella over the paid cloud services — KAS Build / Update / Submit / Credentials (one-to-one with EAS Build/Update/Submit/credentials) — plus the companion Keel Go (free tester app) and Dev Client (custom tester). Each layer is independently usable and independently billed, but together they give China-based RN developers a complete workflow.
Naming:
KAS = Keel Application Servicesis the cloud umbrella (≡ EAS), NOT just the credential vault — that’s KAS Credentials. CLI verbs staykeel build / submit / publish / rollback / credentials; the cloud processes staykeel-{build,update,push,submit}+credentials-server(KAS Credentials).
| Layer | Expo analog | Status |
|---|---|---|
| Keel SDK (framework) | Expo SDK | Shipped (KeelModuleCore.xcframework + keel-module-core.aar + KeelRuntimeHost) |
| Keel CLI (framework) | Expo CLI | Shipped (@keel-ai/cli) |
| KAS Build | EAS Build | Shipped (metro + Hermes bytecode + Docker pipeline; keel build --local signs .aab/.ipa; iOS/Android via a Mac agent) |
| KAS Update | EAS Update | Shipped (manifest + bsdiff + client SDK + LocalFS BlobStore); production-grade (CDN / custom domains / staged rollout / monitoring) planned |
| KAS Submit | EAS Submit | Design ✓ + driver scaffolds (see submit.md); driver impl in progress |
| KAS Credentials | EAS credentials | Shipped (AES-256-GCM vault + server-side keystore generation + Postgres persistence; verified staging/prod) |
Keel focuses on the iOS + Android mobile target. Web apps use a separate Next.js / Vite stack; the WeChat Mini Program is on the roadmap (a separate WXML pipeline, not reusing RN code).
Keel’s relationship to Mortar: fully decoupled, no dependency. Keel works with any backend; wiring up Mortar is the same as wiring up Supabase / Firebase / your own backend (the standard Mortar SDK, no Keel-specific plugin).
The 5-layer architecture
┌──────────────────────────────────────────────┐
│ Developer machine │
│ ┌──────────┐ ┌────────────┐ │
│ │ Keel CLI │ │ Keel Go │ ← scan QR │
│ └────┬─────┘ └────────────┘ paste URL │
│ │ │
│ │ push tarball │
└───────┼───────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ KAS Build (cloud build) │
│ metro → Hermes byte-compile │
│ → ipa / apk │
│ [build host: Aliyun ECI / FC] │
└────┬─────────────────────────┬─────────┘
│ │
artifact → Keel OSS metadata
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ KAS Update │ │ KAS Submit │
│ (manifest + │ │ (App Store + │
│ bsdiff + CDN │ │ Huawei/Xiaomi/│
│ + rollout) │ │ OPPO/vivo/ │
│ │ │ Tencent MyApp)│
└────────┬────────┘ └─────────────────┘
│
client pulls bundle
│
▼
┌─────────────────────────────────────────────┐
│ Keel SDK (inside the user's app) │
│ │
│ iOS: KeelModuleCore.xcframework │
│ Android: keel-module-core.aar │
│ │
│ Optional plugins: │
│ import { mortar } from '@mortar/client' ← if using the Mortar backend │
│ import { Stack } from '@keel-ai/router' ← if using App Router │
│ import { wechat } from '@keel-ai/wechat' ← China-first first-class module │
└──────────────────────────────────────────────┘
Layer 1: Native SDK
The core runtime you embed into an existing RN app or a new app.
The two artifacts
| Platform | Artifact | Path |
|---|---|---|
| iOS | KeelModuleCore.xcframework (fat binary) | keel/build/ios/KeelModuleCore.xcframework |
| Android | keel-module-core.aar | keel/build/android/keel-module-core.aar |
Distribution channels (iOS)
iOS ships on two tracks, SPM by default, CocoaPods as the compatibility path:
| Channel | Scope | Position |
|---|---|---|
| Swift Package Manager | KeelModuleCore.xcframework (core) + most official @keel-ai/* modules | ✅ default |
| CocoaPods | the few official modules whose low-level deps only publish a podspec | 🔁 compat |
SPM is Apple’s officially recommended dependency manager — natively supported in new iOS projects, zero config. Keel defaults to SPM, aligning with where the iOS ecosystem is heading; the few modules whose third-party SDKs don’t yet ship a
Package.swiftkeep a CocoaPods integration path and fold back into SPM as those upstreams migrate. Integrators never have to choose between the two tools — the core goes through SPM, and you pull in whichever pod-only module you need on the side, with no conflict.
The iOS SPM entry point lives in keel/modules-core/. keel/sdk/ is now a pure-JS umbrella — the native (foundation + substrate) on BOTH platforms folded into modules-core (iOS 2026-05-29, Android in the Z restructure), delivered symmetrically as KeelModuleCore.xcframework (SPM) + keel-module-core.aar (maven com.appunvs:keel-module-core). So keel/sdk/ has no android/ / ios/ / Package.swift / Keel.podspec:
keel/modules-core/Package.swift— the only iOS SPM package: a.binaryTargetpointing atkeel/build/ios/KeelModuleCore.xcframework, plus theKeelMacrossource plugin. The host consumes it directly bypath:.keel/modules-core/KeelModuleCore.podspec— a build-time recipe only (react-native.config.jssets ios:null, so it is never autolinked; it is used once when baking the xcframework).- On the npm side,
@keel-ai/keel(the umbrella) lists@keel-ai/modules-coredirectly in itsdependencies, avoiding reliance on leaf peer-dep transitivity (same pattern as Expo’sexpoumbrella).
iOS distribution shape — varies by layer (in transition)
The presence of Package.swift vs *.podspec at each layer:
| Layer | Package.swift | podspec | Meaning |
|---|---|---|---|
modules-core | ✅ | ✅ (build-time only) | the podspec is used once by modules-core/packaging/build-ios.sh when xcodebuild bakes KeelModuleCore.xcframework; consumers only ever see the SPM .binaryTarget |
packages/* (kernel leaves + 2 signin) | ✅ | ❌ | SPM-only: all are React-free SPM source targets, the podspec removed; includes the kernel leaves + apple-signin / google-signin (moved from modules/ after going React-free on 2026-06-03; google pulls GoogleSignIn-iOS transitively via SPM); the host pulls them in by path: |
modules/* (vendor) | ✅ | ❌ | the 5 vendor modules (alipay/amap/push/umeng/wechat): React-free SPM source targets (they have a Package.swift, no podspec) |
modules-autolinking implements both paths: link / install emit a per-module Podfile + Gradle snippet (on iOS there is now no internal consumer — signin also moved to SPM on 2026-06-03; the snippet is only for external standalone distribution); autolink scans <host>/node_modules and generates KeelModulesProvider.{swift,kt}, registering each module with KeelModuleRegistrar. The React-free SPM modules are still declared by hand via path: in the host’s Package.swift / project.yml, but their registration entries are generated by autolink.
See keel/packaging/release.sh for the release flow (a one-shot local release-snippet generator; zero CI); for private / enterprise channels see enterprise-distribution.md.
Android has a single channel: keel-module-core.aar distributed via Maven (Maven Central planned), no multi-channel needed.
Public API (at the native layer)
| Class / interface | Platform | Purpose |
|---|---|---|
KeelRuntimeHost | iOS + Android | the substrate that mounts an AI bundle; loadBundle produces an RN rootView the host embeds via SwiftUI UIViewRepresentable / Compose AndroidView |
KeelSDK | iOS + Android | global init (inject license / endpoint) |
Class names:
KeelRuntimeHost/KeelSDK. The module / framework names (KeelModuleCore.xcframework/import KeelModuleCore/keel-module-core.aar) have always been Keel-branded.
Public API (at the JS layer)
// Backend — optional plugin (@mortar/client is not in the default bundle)
import { mortar } from '@mortar/client';
mortar.from('todos').select(...);
mortar.auth.signIn(...);
// File-based routing — optional plugin (added)
import { Stack, Tabs } from '@keel-ai/router';
// or keep using react-navigation's imperative style (compatible)
// China-first first-class modules — optional (npm install on demand)
import { wechat } from '@keel-ai/wechat';
import { alipay } from '@keel-ai/alipay';
New Architecture on by default
The Keel SDK tracks RN 0.85.2 and enables the New Architecture by default:
- Fabric (new renderer) — async, concurrent rendering
- TurboModules (new native modules) — lazy loading, 30%+ lower startup latency
- Bridgeless (no RN bridge) — no serialization overhead between JS and native
Unlike Expo, which conservatively waits for users to opt in, Keel treats the New Architecture as a product feature — bundle startup speed / performance is 30%+ better than an Old Architecture RN bundle, one of Keel’s selling points over a plain RN project.
Internal technical detail (invisible to developers)
Each KeelRuntimeHost carries its own Hermes runtime; JS state does not leak across instances. This opens the door to super-app / multi-tenant / multi-bundle scenarios in higher-level apps. A typical RN developer is fine with a single instance, matching Expo’s behavior.
Layer 2: CLI (@keel-ai/cli)
Analogous to npx expo / npx eas, but on China npm mirrors with a Chinese-language UI by default.
npm install -g @keel-ai/cli
keel create my-app # new project (pick one of 5 templates)
keel start # local dev server (metro + Keel Go pairing)
keel doctor # check the environment (Node / Java / Xcode / iOS SDK version compat)
keel install <pkg> # RN-aware dependency install (version-compat checks)
keel build ios # trigger a cloud iOS build
keel build android # Android
keel build dev-client # build the project's Dev Client
keel publish <bundle> # push a new bundle (OTA: upload + promote)
keel submit ios --to=appstore # auto-submit to the iOS App Store (planned)
keel submit android --to=huawei # Huawei AppGallery (planned)
Implementation: a Node CLI (distributed on npm) that calls the Keel backend’s HTTP API. It does no building locally — it only packs a tarball and uploads it.
Note: there is no
keel build web/keel deploy --target=web— Keel does not do the web target (see the note at the top).
Layer 3: Build
Analogous to EAS Build. A cloud build service. keel build ios triggers:
- The CLI packs the project source (with
package.json) into a tarball and uploads it to the Build endpoint - The backend spins up an ephemeral container on Aliyun ECI / FC, pulling an image (pre-installed metro + Hermes + iOS / Android toolchain)
- The container runs
metro bundle→hermesc→ the platform’s packaging script - The artifact (
.ipa/.apk) lands in Keel’s own OSS bucket - The CLI gets back a download URL; it pulls it locally or feeds straight into the Update / Submit flow
Code paths:
keel/cloud/build-image/(metro + Dockerfile) +keel/cloud/internal/cloudbuild/(the Go service) +keel/cloud/cmd/build-server/(the HTTP entry). The external product name is KAS Build (KAS = Keel Application Services, the cloud umbrella).
Layer 4: Update
Analogous to EAS Update. The OTA protocol layer plus production-grade distribution infrastructure in one. It includes:
- Protocol: the manifest API + client SDKs (TS / iOS Swift / Android Kotlin)
- Transfer optimization: bsdiff4 incremental patch updates
- Storage: a bundle blob store (LocalFS today, Aliyun OSS / S3 planned)
- Distribution: CDN edge cache (planned, Aliyun CDN) + custom domains + HTTPS
- Release control: channels (production / staging / development) + staged rollout (10%→50%→100%, planned) + one-click rollback (planned)
- Monitoring: pull success rate / latency / error rate (planned, Feishu / DingTalk alerts)
- Optional push (
@keel-ai/push): JPush / GeTui, triggering clients to pull a new bundle immediately
Self-operated, decoupled from Mortar: KAS Update manages its own manifest DB (PG / SQLite) + its own Aliyun OSS + its own CDN. It does not depend on Mortar primitives. Running KAS Update incurs no Mortar bill.
Code paths: keel/cloud/internal/update/ (the Go service) + keel/cloud/cmd/update-server/ (the HTTP entry) + keel/packages/updates/ (the @keel-ai/updates npm package: three parallel client implementations in TS + Swift + Kotlin).
See update.md for details.
Layer 5: Submit
Analogous to EAS Submit. Auto-uploads the ipa / apk produced by keel build to app stores for review. Planned.
Supported targets:
- iOS: the App Store Connect API
- China’s 5 major Android stores: Huawei / Xiaomi / OPPO / vivo / Tencent MyApp (each has a different API; implemented in priority order by market share)
- International Android: the Google Play Console API (optional, for overseas users)
Each store has a different review mechanism; the CLI helps users fill in metadata (screenshots / description / privacy policy), monitors review status, and gives feedback when a review fails.
First lands the iOS App Store + Huawei + Xiaomi + OPPO; vivo / Tencent MyApp later.
See the Submit section of roadmap.md for details.
Modules API (horizontal extension, mapping to the Expo Modules API)
A horizontal extension above the 5 layers — @keel-ai/modules-core lets the Keel team (or any RN module author) write a Keel module, publish it to npm, and have a consumer project npm install + autolink it. It maps to the Expo Modules API (a declarative spec + native iOS/Android + TS bindings packaged together), but without depending on the expo runtime.
Flat distribution model
Like Expo, there are no tier levels. Two kinds of artifact come out of this repo:
| Artifact | Contents | Integration |
|---|---|---|
| SDK runtime | KeelModuleCore.xcframework + keel-module-core.aar (just the KeelRuntimeHost mount + the Hermes runtime) | the host app links the SDK binary via pod / gradle |
| Keel npm packages | individual npm packages under keel/packages/<name>/ | the host app npm install @keel-ai/<name> + standard RN autolink |
Whether it’s Keel’s own Keel Go tester, Fabric’s host shell, or a third-party RN project, they all go through the same autolink path — the dependencies declared in the host’s package.json decide which native modules go into the binary. There are no “special modules embedded in the SDK” — apart from the one core runtime KeelRuntimeHost, every capability is its own npm package (including OTA Update, see @keel-ai/updates).
If a downstream app wants to gate which modules an AI bundle may import, it maintains an allowlist on its own side — Keel does not bake in tiers to support that kind of gating.
The module.yml declaration
name: keel-module-wechat
platforms: [ios, android]
ios: { pod: KeelModuleWeChat, min_ios: "13.0" }
android: { aar: keel-module-wechat, min_sdk: 23 }
js: { entry: ./src/index.ts }
peer_runtime: "^1.0.0"
The current official Keel modules
Spread across three top-level directories by role: feature packages in keel/packages/, vendor modules in keel/modules/, dev tooling in keel/tools/.
Platform utilities — keel/packages/ (mapping to expo-* platform metadata + OTA):
@keel-ai/application— app metadata (bundle ID / name / version / build number)@keel-ai/constants— runtime constants (status bar height / platform identity / simulator detection)@keel-ai/device— device info (model / manufacturer / OS name + version)@keel-ai/asset— bundle asset resolution + remote asset cache download@keel-ai/font— runtime custom-font registration@keel-ai/updates— the OTA bundle distribution client (manifest + bsdiff + channel + rollback), mapping toexpo-updates. Three parallel implementations: TS (JS-side self-update), Swift (the iOS host shell’s native launcher), Kotlin (Android)
China-first vendor integrations — keel/modules/ (the gap in domestic RN modules, scaffolded out by create-keel-module):
keel-module-wechat— WeChat pay / login / share / Mini Program jumpkeel-module-alipay— Alipay pay / real-name / Mini Program jumpkeel-module-amap— AMap maps / location / route planningkeel-module-push— a push abstraction layer (JPush + GeTui backends)keel-module-umeng— Umeng analytics
Auth providers — @keel-ai/apple-signin / @keel-ai/google-signin (native, React-free SPM, under keel/packages/, pulled by @keel-ai/keel as default kernel; moved from modules/ on 2026-06-03) / @keel-ai/auth-session (pure TS, under keel/packages/)
App framework — keel/packages/: @keel-ai/router (file-system routing, mapping to expo-router). Its companion compiler plugin router-metro-plugin is in keel/tools/
Dev tooling — keel/tools/: bundle-atlas (bundle content auditing) / create-keel-project / create-keel-module
CLI
# Scaffolding lives in the standalone create-keel-module package (Expo pattern)
npm create keel-module <name>
# modules-core's keel-module CLI only does daily-dev
keel-module validate # validate module.yml + sources
keel-module link # add to the host app's package.json + emit autolink snippet
keel-module install # write the snippet into Podfile / settings.gradle.kts (idempotent)
keel-module test # run module tests in a minimal host-app context
keel-module publish # validate + bump version + npm publish
See modules-core/README.md for details.
Companions: Keel Go + Dev Client
Keel Go
Same usage as Expo Go: developers download the free Keel Go app from the App Store / China’s 5 major stores; scan a QR / paste a URL to load any Keel bundle.
- What it can run: whichever Keel modules Keel Go declares at compile time in its own
package.jsonare in the binary; a bundle that only uses those works out of the box - Upside: zero config, see results on a real device within 5 minutes
Dev Client
keel build dev-client auto-generates a sideloadable tester app for the project:
- includes all of the project’s native modules (the ones in
package.json+ the autolinked ones) - iOS: sideload to a test device / TestFlight distribution
- Android: sideload the .apk / enterprise distribution on domestic stores
Analogous to the Expo Dev Client. Purpose: when a project’s native modules exceed what’s already linked into the Keel Go binary, the Dev Client re-links a tester that includes exactly the native modules this project needs.
Relationship to Mortar (fully decoupled, no dependency)
Keel has no special integration with Mortar — Mortar is just one of many BaaS options:
- Not bundled by default: the Keel SDK does not auto-inject a Mortar client
- No
@keel-ai/mortarplugin package: users wire up Mortar through the standard Mortar SDK (npm install @mortar/client), the same way they’d use@supabase/supabase-jsfor Supabase orfirebasefor Firebase - Usage:
// Your own backend:
fetch('/api/todos');
// Mortar:
import { mortar } from '@mortar/client';
mortar.from('todos').select(...);
// Supabase / Firebase / ...:
import { createClient } from '@supabase/supabase-js';
// all three are fine, no conflict
Why fully decoupled:
- so Keel users aren’t forced onto Mortar (Supabase / your own backend / Firebase all work)
- so Mortar users aren’t forced onto Keel (any RN / web / native works, including Expo)
- so each side can be sold independently to non-overlapping customers, with no reverse references
- Mortar’s docs never mention Keel; Keel’s docs only mention Mortar in passing as “one of many BaaS options”
- internal dogfood: appunvs uses the standard integration path, with no back doors
Internal tech (not on the product marketing surface)
- per-instance Hermes: one independent Hermes runtime per
KeelRuntimeHost(not a shared runtime with isolated contexts; a genuinely separate runtime). Opens the door to super-app / multi-bundle coexistence scenarios - SDK ABI version: the
sdk_versionfield inkeel/version.json; adding / removing / re-signing a native public API = a bump
Evolution path
See roadmap.md. Current progress:
- Shipped: SDK + Build ✅ shipped
- In progress: CLI + docs site + Update (OTA protocol + client SDKs on all three platforms) ✅ done; productionizing Update (CDN / custom domains / staged rollout / monitoring) + App Router + Keel Go + Open Governance to do
- Planned: Modules API + Dev Client + the 5 China modules +
@keel-ai/push+ Submit (incl. China’s 5 major Android stores) - Later: Bundle Atlas + VS Code extension + monitoring dashboard and other developer tools
- Later: enterprise edition (cross-cutting: private deployment / SSO / audit log / SLA)
Governance + open source
Keel is an Apache 2.0 open-source project. See governance.md.
- public GitHub repo + RFC process
- public roadmap
- commercial services (Pro / Team / Enterprise) sell hosting + SLA + first-class operations, and do not cripple open-source features