Machine-translated draft — terminology + flow still being reviewed.

Mobile — architecture spec

Status (2026-05-15): Mobile is the original Fabric product type — RN/Keel-based mobile apps generated by AI, hosted in the iOS

  • Android host shells in appunvs/{ios,android}/. Pipeline is the longest-running of the four project types; treat this doc as the baseline reference the other project types (Web / Desktop / WeChat MP) diverge from.

Mobile is the first of Fabric’s 4 project-type pipelines (see project-types). Output target: a React Native bundle compiled to Hermes bytecode, loaded into a KeelView inside the user’s host shell on real iOS / Android devices. Reference: how Expo Go loads bundles into a tester app — but the appunvs host shell additionally provides Chat + Profile tabs and lives in the user’s own pocket.

Pipeline overview

        Chat turn
  (user → fabric-server AI)

            ▼ fs_write tool calls into project workspace
  ┌──────────────────────────┐
  │  fabric-server workspace │   = git repo at fabric/server/workspaces/<proj>/
  │  (TypeScript / TSX +     │     RN-flavoured AI prompt; Tier 1 + 2
  │   keel/MODULES allowlist)│     module allowlist enforced at bundle time
  └──────────────────────────┘

            ▼ publish_project tool fires the build
  ┌──────────────────────────────────────┐
  │  cloudbuild.HarnessBuilder           │   POST harness
  │  {ProjectType: "mobile"}             │   /v1/projects/{id}/build
  └──────────────────────────────────────┘


  ┌──────────────────────────────────────┐
  │  harness: preview.Manager.Build      │   metro `react-native bundle`
  │  in the project's runner-mobile      │   inside the project's own runner
  └──────────────────────────────────────┘


  ┌──────────────────────────────────────┐
  │  harness uploads the bundle to the   │   artifact.Store.Put →
  │  content-addressed object store      │   _artifacts/<sha256hex>
  │  and returns ONLY a manifest:        │
  │  {entry:"index.bundle", files[], …}  │   no bytes cross fabric-server
  └──────────────────────────────────────┘


  ┌──────────────────────────────────────┐
  │  fabric-server records the manifest  │   app_bundles.manifest (jsonb)
  │  on the bundle row and signs a       │   → Bundle.URI = presigned
  │  short-lived URL for the entry       │      download URL
  └──────────────────────────────────────┘

            ▼  SSE event /project/events → host shell
  ┌──────────────────────────┐
  │  User's iOS/Android host │
  │  Preview tab refreshes     │
  │  KeelView, loads new     │
  │  bundle from URL         │
  └──────────────────────────┘

Components

Host shells (appunvs/{ios,android}/)

The user-facing apps that live on real devices. Both shells are 3-tab apps with identical IA:

TabiOS implementationAndroid implementation
ChatSwiftUI ChatView (message list + tool pill UI + input bar)Compose ChatScreen
PreviewSwiftUI host for KeelView (UIView)Compose host for KeelView (FrameLayout)
ProfileSwiftUI account / device / usage UICompose equivalent

The shells embed Keel (Keel.xcframework / keel-sdk.aar) for bundle runtime. They do not depend on KAS Update — the mobile host shell fetches bundles directly from fabric-server’s artifact endpoint (KAS Update is for non-appunvs RN devs).

Auth: each host shell holds a session JWT (issued by fabric-server’s /auth/signup or /auth/login); the JWT scopes API calls to the User’s workspace. See auth.

Keel SDK (keel/)

Provides KeelView, KeelSDK, KeelHostModule. Per-instance Hermes runtime — every KeelView.loadBundle(at:) boots a fresh runtime; reset() tears it down. Cross-bundle state cannot leak.

Host bridge surface: AI bundles import { host } from '@keel-ai/host' for identity / storage / network / publish / subscribe.

Tier 1 + Tier 2 module allowlist (keel/MODULES) is enforced at metro bundle time by keel/cloud/build-image/metro.config.js.

fabric-server (fabric/server/)

Backend for everything that’s not on-device:

  • Auth: users / devices tables, session + device JWT (see auth)
  • AI agent loop (internal/ai/): one Chat turn → stream of Token / ToolCall / ToolResult / Finished frames. Tools include fs_write (write to workspace git repo) and publish_project (trigger build → publish). See providers for the 3 production AI providers + 1 Stub.
  • Workspace (internal/workspace/): per-Project bare git repo; every AI fs_write becomes a commit.
  • Build (internal/cloudbuild/): just the seam. The Builder interface returns an artifact.Manifest, and its one implementation is HarnessBuilder (ProjectType: "mobile" for this pipeline) — a thin client that POSTs /v1/projects/{id}/build to the harness. metro runs in the project’s runner-mobile container, not here; fabric-server has no Node toolchain and no source checkout. A harness is required — the old LocalStub / DockerBuilder (appunvs/cloud-build-mobile) are deleted, and there is no local fallback.
  • Artifact (internal/artifact/): the READ side of the content-addressed store — artifact.S3 against an S3-compatible bucket (Aliyun OSS in prod/staging, MinIO in local dev), the same bucket the harness uploads to (_artifacts/<sha256hex>). LocalFS is unit-tests only. SignURL mints the short-lived download URL the host shell fetches the bundle from.
  • Project events (/project/events SSE endpoint): fanout to the user’s host shells when a new bundle becomes ready.

What’s in Mobile GA

ComponentStatus
iOS host shell (Chat + Preview + Profile)✅ shipped
Android host shell (Chat + Preview + Profile)✅ shipped
Keel SDK (Keel.xcframework, keel-sdk.aar)✅ shipped
KeelView + per-instance Hermes✅ shipped
Host bridge (@appunvs/host)✅ shipped
Tier 1 / 2 module allowlist✅ shipped
fabric-server auth + AI agent loop✅ shipped
metro bundling in the harness (runner-mobile + cloudbuild.HarnessBuilder{ProjectType:"mobile"})✅ shipped
Content-addressed publish (harness uploads → manifest → app_bundles.manifest, goose 00013)✅ shipped
/project/events SSE fanout✅ shipped
cloudbuild.DockerBuilder · cloud-build-mobile docker image (server-side metro)❌ retired — superseded by the harness path above

Project-events SSE shape

Same shape across all 4 project types (see architecture for the cross-type contract):

event: bundle_ready
data: {
  "project_id": "proj_abc",
  "version": 17,
  "runtime": "rn_keel",
  "bundle_uri": "https://<bucket>.<oss-endpoint>/_artifacts/<sha256hex>?<signature>"
}

bundle_uri is the short-lived presigned object-store URL minted from the manifest’s entry hash (artifact.Store.SignURL) — the host fetches the bundle straight from the bucket, never through fabric-server.

Host shell’s Preview tab subscribes via SSE. On bundle_ready for the currently displayed project, it calls KeelView.loadBundle(at: bundle_uri) to remount.

Tier 1 / 2 / Forbidden modules

keel/MODULES is the canonical allowlist of npm packages AI bundles may import:

  • Tier 1: core RN + first-party Keel modules (@keel-ai/router / @keel-ai/wechat / etc). No friction.
  • Tier 2: vetted community modules (react-native-svg, react-native-screens, etc). Pre-bundled into the build image; no autolink dance.
  • Forbidden: anything with native modules that aren’t on Tier 1 or 2. Build fails at metro time with a clear error.

Keel Go runs the same allowlist — non-Tier-1 native modules require Keel Dev Client.

Connection to Mortar BaaS

AI bundles import { mortar } from '@mortar-ai/client' for AppUser data (auth / data / files / realtime). Mortar is a 可选 plugin — bundles can also call host bridge network.request to hit any HTTP backend.

⚠️ Fabric itself does NOT run on Mortar — user accounts, project metadata, ai_turns, builds, bundles all live in Appunvs’s own infrastructure (fabric-server’s Postgres + Redis, the harness’s runners, and the Appunvs-operated artifact bucket) — same pattern as an AI builder not running its own editor on the BaaS it provides users. Mortar holds only the AI bundle’s own end-user-facing data.

See architecture § 责任划分.

Decision points (revisit if pain surfaces)

  1. /project/events SSE vs WebSocket. An early build used WebSocket; Mobile switched to SSE-based events (simpler client; works through more proxies; reconnect handled by the standard EventSource API). Revisit if bidirectional push from host → fabric-server becomes common (currently rare; most writes are HTTP POST).
  2. LocalFS artifact store vs OSSsettled. Published artifacts always live in S3-compatible object storage, never on a service’s local disk: Aliyun OSS via its S3-compatibility endpoint in prod/staging (bucket appunvs-artifacts, cn-shenzhen), MinIO locally (shared/deploy/compose.local.yml). LocalFS survives only in unit tests. This is what lets the harness write and fabric-server read without any shared volume.
  3. Host shell branding. Mobile ships as “Fabric” on both stores. A later iteration revisits whether enterprise customers need their own branded shell (likely yes; planned for the Enterprise pack).

What Web/Desktop/WeChat inherit from Mobile

The non-mobile project types all reuse Mobile’s substrate:

  • Same fabric-server (auth, AI agent, workspace, project state machine, SSE events)
  • Same cloudbuild.Builder interface — and in fact the same single implementation: web / mobile / desktop all use cloudbuild.HarnessBuilder, differing only by the project_type it sends, and all return the same artifact.Manifest. Each compiles in its own harness runner. (The old server-side *DockerBuilders + cloud-build-* images, MultiBuilder / MultiResult, and every local stub were deleted.) WeChat is the one exception — cloudbuild.WeChatBuilder publishes to Tencent and returns a receipt, not a manifest.
  • Same project-events SSE schema
  • Same host shell Chat + Profile tabs (Preview tab swaps out per project type — KeelView for Mobile, webview iframe for Web/Desktop, QR for WeChat)