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

Architecture & implementation plan

⚠️ This doc only describes the internal architecture of the Mobile pipeline. Appunvs as a whole is a multi-project-type AI builder — see project-types for the strategic positioning of the four independent pipelines (Mobile RN / Web Vite / Desktop Electron / WeChat WXML). The Web / Desktop / WeChat pipelines are decoupled from the Mobile/RN/Keel chain described below and don’t depend on any component in this doc.

Appunvs is a Creator-operated, AI-driven mobile-app generation SaaS (Mobile project type):

  • User edits code via AI conversation, publishing an RN bundle
  • Bundle previews inside the Preview on the User’s own device; to expose it to real end-users, publish it to a Mortar workspace
  • End-user data (records produced by AppUsers inside the AI-generated app) lands / syncs through Mortar’s feature/data + feature/realtime

The iOS + Android native host shells embed the Keel SDK (an independent RN platform product, Apache 2.0); fabric-server handles User auth, the AI agent, build, artifact hosting, and the Project workspace (git). The AI bundle in the Preview talks to Mortar Cloud directly via Keel’s host bridge + the standard Mortar SDK (@mortar-ai/client). Appunvs’s project template defaults to npm installing this SDK, but users can swap in another BaaS.

About Hosting: Appunvs and Keel are two products, with different Hosting responsibilities —

Component topology

                ┌──────────────────────── fabric-server (Go 1.22+) ────────────────────────┐
                │                                                                  │
                │  auth · ai · cloudbuild · artifact · project · workspace(git) ·         │
                │  /project/events (SSE-based events)                                  │
                │                                                                  │
                │  SQLite/Postgres: users · devices · projects · bundles · ai_turns   │
                │  Redis:           seq · project_events fanout                        │
                │  Disk:            per-project bare git repos (workspace/)            │
                │  Object:          published artifacts (S3-compatible: OSS / MinIO)   │
                │                   written by the harness, read by fabric-server       │
                └──────────┬───────────────────────────────────────────────────────┘
                           │  HTTP + SSE-based events via /project/events

                  ┌────────┴───────┐
                  │                │
              iOS host          Android host         (User's own devices)
              SwiftUI           Compose
                  │                │
                  │ Preview tab loads bundle into KeelView (per-instance Hermes)

                  └─→ AI bundle inside Preview

                          │ via host bridge (@appunvs/host)


                      Mortar SDK (TypeScript) ─────────► Mortar Cloud
                                                        feature/auth · data · files
                                                        realtime · compute · usage

Mortar Cloud is a fully independent product and not part of Appunvs; fabric-server itself does not call Mortar — all Mortar traffic comes from inside the AI bundle process via the SDK.

Repository layout

appunvs/
├── appunvs/{ios,android}/   # Native host shells
├── keel/                 # Keel — China-friendly RN platform (independent product;
│                            #         ships as Keel.xcframework / keel-sdk.aar today,
│                            #         Keel.xcframework / keel-sdk.aar after the rebrand)
├── fabric/server/
│   └── internal/
│       ├── auth/            # JWT signer; session / device flavors
│       ├── handler/         # /project, /keys, /project/events, /ai/turn
│       ├── store/           # SQLite (users, devices, projects, bundles, ai_turns)
│       ├── project/         # project.Service: workspace commit → build → artifact → publish
│       ├── workspace/       # per-Project bare git repo (go-git); AI fs tools write here
│       ├── artifact/        # content-addressed store READ side (S3: OSS / MinIO) + manifest
│       ├── cloudbuild/      # HarnessBuilder: POSTs to the harness, relays the manifest (no bytes)
│       ├── ai/              # AI agent loop (Anthropic / OpenAI-compat / Gemini)
│       ├── billing/         # subscription gate + ledger
│       └── pb/              # generated bindings from shared/proto (run gen.sh)
├── mortar/                  # Independent BaaS product
└── shared/proto/            # canonical wire schema (auth/project/ai/…)

Core objects

ObjectWhere it livesNotes
Projectapp_projects tableThe user-owned project unit; state = draft | published | archived
WorkspaceBare git repo on fabric-server disk (workspace/<project_id>/)Every AI fs_write → one git commit; the build reads the HEAD snapshot
Bundleapp_bundles table + object storage(project_id, version) unique; content-addressed sha256; immutable
AI Turnai_turns tableOne full conversation turn (user text + tool calls + final), stored as JSON

Two independent data flows

1. Edit flow (User Chat → Build → Publish)

User Chat ── POST /ai/turn ──► fabric/server/ai (agent loop)

                                       ▼ tool: fs_write
                                  workspace.Repo.Commit (git commit on main)

                                       ▼ tool: publish_project
                                  project.Service.BuildAndPublish

                                       ▼ cloudbuild.HarnessBuilder (ONE type for web / mobile /
                                       │   desktop — only project_type differs)
                            POST harness /v1/projects/{id}/build

                                       ▼ harness: preview.Manager.Build — compiles in the project's OWN runner
                            web → vite build dist/  |  mobile → metro react-native bundle
                            desktop → electron-builder installers

                                       ▼ the harness uploads every output file, sha256 content-addressed,
                                       │   DIRECTLY to the object store
                            artifact.Store.Put(...)  →  _artifacts/<sha256hex>

                                       ▼ only a MANIFEST comes back: {entry, files:[{path,hash,size}], size_bytes}
                            app_bundles.manifest  +  app_projects.current_version


                            ai_turns.Insert (including cost_cents)


                            project_events SSE: push project_version_update to the User's own devices


                            The Preview tab on the User's device auto KeelView.loadBundle to remount

Publish is owned by the harness: the compile runs in the project’s own runner container, and the harness uploads the outputs content-addressed to the object store itself — not a single artifact byte passes through fabric-server (the old “base64 tree in the JSON response + a server-side webdeployer writing the tree” model is gone). fabric-server only records the manifest on app_bundles.manifest (jsonb, goose migration 00013_bundle_manifest), then:

  • Web / Desktop: GET /web-preview/<project>/<version>/*path resolves the request path through the manifest → hash → object store and streams it back (fabric/server/internal/handler/webpreview.go); an unknown extension-less path falls back to index.html for SPA routing
  • Mobile (single RN bundle): a short-lived signed URL is issued for the manifest’s entry so the host downloads straight from the object store

Publish therefore requires a harness — there is no local-stub fallback.

2. End-user data flow (AI bundle ↔ AppUser ↔ Mortar)

Inside the AI bundle (RN code running in the Preview)

   │  obtains the mortar SDK client via the host bridge


mortar.auth.signIn(email, pw) ──► Mortar Cloud /v1/{ns}/auth/login → AppUser session JWT
mortar.from('todos').select(...) ──► Mortar Cloud /v1/{ns}/db/todos → rows
mortar.realtime.subscribe('todos', cb) ──► Mortar SSE /v1/{ns}/realtime/todos
mortar.storage.upload('avatar', bytes) ──► Mortar OSS


All reads / writes land in Mortar Cloud and sync in realtime to other AppUsers in the same tenant

This flow never goes through fabric-server — fabric-server only matters during User editing. Once the bundle is in use, fabric-server steps off the stage.

Preview cloud-build contract

A bundle loaded into the Preview must not touch the host app’s state, tokens, MMKV, or filesystem.

Two layers:

  1. Runtime isolation (per-instance Hermes runtime):
    • Each KeelView carries its own jsi::Runtime; JS state never leaks across bundles (reset = destroy)
    • Not a WebView, not an iframe — a real native Hermes
  2. Data credential isolation: a bundle inside the Preview holds only limited credentials — it can hold a tenant token issued by Mortar (to call the Mortar SDK), but cannot hold the host’s session_token, and cannot call fabric-server management endpoints like /project

Pluggable abstractions

Interfacev1v2+
cloudbuild.BuilderHarnessBuilder (the only implementation, parameterized by project_type = web | mobile | desktop)Harden the harness-side runner isolation (container → Firecracker / FC)
artifact.StoreS3 (S3-compatible: Aliyun OSS in prod/staging, MinIO locally; LocalFS is unit-tests only)AWS S3 / Cloudflare R2 multi-cloud
ai.EngineStubEngine (CI echo) / HarnessEngine (proxies to fabric-harness)multi-model router + fast-apply live in the harness

Mortar integration (two orthogonal layers)

Fabric and Mortar Cloud are two independent products that dogfood each other:

  • Fabric itself (User accounts / project metadata / ai_turns / project builds / git workspaces / bundle storage) — all live in Appunvs’s own infrastructure: fabric-server’s Postgres + Redis, the harness’s runners + worktrees, and the Appunvs-operated artifact bucket. Not running on Mortar; same pattern as an AI builder not running its own editor on the BaaS it provides users
  • When User publishes a project for real end-users — the bundle attaches to a Mortar workspace; AppUser data / files / realtime sync all go through Mortar Cloud

Responsibility split

CapabilityOwnerWhere it lives
User account / device / session JWTAppunvs fabric-serverfabric-server SQLite/Postgres, see auth
AI conversation engine (/ai/turn, ai_turns table)Appunvs fabric-serverfabric/server/internal/ai
Project metadata / state machine (app_projects)Appunvs fabric-serverfabric-server DB
Project workspace git repoAppunvs fabric-serverfabric-server disk
Project build (vite / metro / electron-builder)fabric-harness (in the project’s runner container)fabric/harness/internal/preview; the fabric-server side is just the thin internal/cloudbuild client
Bundle artifact storagewritten by fabric-harness, read by fabric-serverS3-compatible object store (Aliyun OSS / MinIO), same bucket both sides, key _artifacts/<sha256hex>
Project-events SSE fanout (/project/events, telling the User’s own devices’ Preview to remount the bundle)Appunvs fabric-serverRedis pub/sub
AppUser account / device / JWT (end-user of every published app)Mortar feature/authMortar Postgres, see mortar/docs/auth.md
Data reads/writes inside the AI bundle (mortar.from(...))Mortar feature/dataMortar Postgres
AI bundle file upload / downloadMortar feature/filesMortar OSS
AI bundle SSE realtime subscriptionMortar feature/realtimeMortar SSE broker
User-defined cloud function (HTTP-callable)Mortar feature/computeMortar FC
Mortar usage / credit ledgerMortar feature/usageMortar Postgres

Project → Mortar workspace bridging

When a User publishes a project → fabric-server calls Mortar’s control plane to create / associate a Mortar workspace (one project ↔ one Mortar workspace; its Tiny tier is free). For how the mapping is stored, see internal/store.

When the AI bundle launches inside the Preview, the host bridge injects:

  • The Mortar tenant for this project
  • A Mortar API client derived from a project-scoped session JWT (short-lived user token, can only access this tenant)

The bundle just import { mortar } from '@appunvs/host' and call mortar.from(...) / mortar.auth.signIn(...), with no need to handle tenant routing or token renewal itself.

⚠️ “Workspace” means two things

See CONVENTIONS.md:

  • Fabric workspace = the User’s workspace (ws_…), also the SSE fanout key
  • Mortar workspace = a project tenant on Mortar Cloud (keyed by tenant_id)

Same word, different things — disambiguate by context.

Integration boundaries

BoundaryProtocolClient
User host shell → fabric-serverHTTP + SSE-based events via /project/eventsiOS / Android host
AI bundle → Mortar CloudMortar HTTP API + derived user tokenInside the bundle process (Mortar TypeScript SDK running in Hermes)
fabric-server → Mortar control plane (only when creating / deleting workspaces)Mortar admin API + platform-level API keyfabric-server
Mortar → Aliyun RDS / OSS / Tair / FCEach cloud’s native SDKMortar Go binary

Pricing has two orthogonal layers: the Fabric subscription (pricing-strategy) and Mortar Cloud (mortar/docs/pricing.md) are billed independently. See pricing-strategy § Mortar integration.

Appunvs self-run hosting infrastructure {#appunvs-self-run-hosting-infrastructure}

Appunvs runs its own publishing / distribution infrastructure for each project type, managed per project type. This is NOT the same as Keel’s 5 layers — Keel only handles the Mobile path (Build + Update + Submit); other project types’ hosting is Appunvs’s responsibility.

Mobile (RN/Keel pipeline)

Handled by Keel’s own services; Appunvs does not duplicate them:

ServiceProviderUse
BuildKAS Buildmetro + Hermes bytecode → ipa / apk
UpdateKAS Updatemanifest + bsdiff CDN distribution → host shell + Keel Go
SubmitKAS SubmitApp Store + 5 top Chinese Android store submission
BaaSMortarAppUser data inside the AI bundle

Fabric-server calls the KAS Build + KAS Update HTTP APIs, going through the same path as external Keel users (dogfood).

Web (Vite pipeline) — Appunvs self-run

ServiceContentStatus
Web Buildvite build runs in the harness’s runner-web container (cloudbuild.HarnessBuilder with project_type=web, POSTing to the harness /v1/projects/{id}/build) — the fabric-server container has no Node toolchain and no source checkout. A harness is required; there is no local-stub fallback. (The old server-side WebDockerBuilder / cloud-build-web / ViteLocalBuilder are all deleted.)Web
Web HostingThe harness uploads every dist/ file sha256-content-addressed to Appunvs’s self-run object store (Aliyun OSS, bucket appunvs-artifacts); fabric-server serves them per-path from /web-preview/<project>/<version>/ by resolving the manifest, with Aliyun CDN in front. Every project gets a default <projectid>.preview.appunvs.com subdomain + custom-domain option. No overseas CDN region is planned — individual creators going overseas are constrained by app-store distribution, not CDN; we only revisit if enterprise compliance requirements demand itWeb
HTTPS certLet’s Encrypt auto-issue + renewal (when user binds a custom domain)Web
MortarShares Mobile’s Mortar workspace (sibling-project model)Existing

Analogy: The China-side equivalent of Vercel / Netlify / Cloudflare Pages — but serves only Appunvs-produced Vite apps, not opened to general users as a Vercel-style commodity cloud (to avoid turning into a generic cloud provider).

Desktop (Electron pipeline) — Appunvs self-run

ServiceContentStatus
Desktop Buildelectron-builder runs in the harness’s runner-desktop container (cloudbuild.HarnessBuilder with project_type=desktop) → .deb (Linux) / .exe (Windows, on x86 hosts via wine). macOS .dmg cannot be produced in that Linux container — it needs a separate mac runnerDesktop
Binary HostingThe harness uploads the release/ installers sha256-content-addressed to Appunvs’s self-run OSS (Aliyun); fabric-server serves them per-path from /web-preview/<project>/<version>/ via the manifest, and the download URL is surfaced to the user in the editorDesktop
Auto-Updater Endpointelectron-updater-protocol-compatible (https://updates.appunvs.com/<projectid>/...); the user’s Electron app checks on startupDesktop
Code SigningmacOS notarization (Apple Developer ID) + Windows EV certDesktop+

Analogy: A hosted Squirrel.Mac / electron-builder + electron-updater.

WeChat Mini Program — WeChat’s own infrastructure

ServiceProviderUse
Buildminiprogram-ci in the harness’s runner-wechat container (cloudbuild.WeChatBuilder.Publish — a separate path from the manifest pipeline above)Uploads straight to Tencent and returns an upload-result.json receipt
DistributionWeChat Open Platform (NOT Appunvs self-run)Trial submission / review / release
DomainWeChat-ownedMini program codes, <appid> URLs

Appunvs at this layer only handles “call miniprogram-ci to upload” — distribution is WeChat’s responsibility. So WeChat Mini Program has no Appunvs-self-run Hosting and no stored manifest (the artifact lives on Tencent); Bundle.URI is the experience QR from the receipt.

Shared layer (across all project types)

ServiceUse
fabric-serverUser account / device auth / AI agent / Project workspace (git) / SSE-based events via /project/events / billing
Mortar BaaSThe AppUser-data backend for every AI-generated app (auth / data / files / realtime)
app.appunvs.com (launch)Web client, main user entry, runs on Appunvs’s self-run CDN
Sentry (self-hosted on Aliyun)Cross-product error monitoring / crash reporting / release tracking. Integrated from GA; each service gets its own DSN:
AI bundles in KeelView (the user’s RN code): @sentry/react-native
Fabric app (host shell, native iOS Swift / Android Kotlin, not RN): sentry-cocoa / sentry-android
Fabric web (Next.js): @sentry/nextjs
Fabric desktop (Electron): @sentry/electron
fabric-server / update-server / build-server (Go backends): sentry-go
Sourcemap + release-tag uploads → cross-service correlation of “which update bundle introduced this crash” — the data source for Update layer’s auto-rollback

Comparison to KAS Update / Submit: KAS Update is Keel-run, serving every RN developer using Keel (not just Appunvs users). Fabric Web Hosting / Desktop Binary Hosting is a Fabric internal service, not open to outside use — to use it you have to be on a Web / Desktop project on Appunvs. That’s the product boundary: Keel is an open toolchain (Apache 2.0 + standalone sellable); Appunvs is a closed AI builder product (PolyForm Noncommercial).

Ingress / TLS

GA deployment topology:

Internet

Aliyun SLB (L4 / L7, handles TLS termination + DDoS + health checks)
  ↓ HTTP
Caddy reverse proxy (inside the Appunvs container cluster)
  ↓ HTTP
fabric-server / update-server / build-server / editor (Next.js)

Why Caddy instead of nginx:

  • Automatic Let’s Encrypt: One-line config for custom domains (update.<customer>.com / editor.<customer>.com); auto-issue + auto-renew. nginx requires certbot + cron, which is operational overhead
  • HTTP/3 / QUIC is on by default — latency win on mobile networks
  • Config syntax: A Caddyfile is ~5× more concise than nginx.conf; at GA, SRE headcount is low, so Caddy is friendlier
  • Performance: Same order as nginx (written in Go, handles high concurrency via goroutines); plenty for current traffic

Aliyun SLB handles TLS because of mainland-China filing / regulatory requirements (traffic SSL termination must happen on Aliyun-owned devices); Caddy handles Let’s Encrypt + internal routing for custom-domain cases.

Implementation order

Already merged:

  • fabric/{ios,android}/ native host shells (Chat / Preview / Profile three tabs)
  • keel/ Keel SDK (internal per-instance Hermes isolation; xcframework + aar)
  • fabric/server/internal/project, artifact, cloudbuild (project build pipeline), ai, workspace interfaces + v1 implementations
  • fabric/server/internal/store/turns (the ai_turns table)
  • project.Service.BuildAndPublish: workspace git commit → harness compiles in the runner → harness uploads content-addressed to the object store → server records the manifest → publish
  • AI engines: StubEngine (CI echo) + HarnessEngine (sole AI for user code turns, proxies to fabric-harness)
  • llmgw: the LLM gateway. The provider layer belongs to neither agent — it is neutral infrastructure. harness (user code turns: sandboxed / BYOK / per-user billing) and the assistant (maintainer ops turns: trusted, platform-paid) share no code, only this hop. The contract is deliberately not OpenAI-shaped: events carry thinking / signature, and the signature must be replayed verbatim on the next tool-using request — OpenAI has no such field.
  • fabric/assistant: Fabric’s internal AI employee — the maintainer-side ops agent, its own service. A profile = {tools + system prompt + trigger}; swapping the profile swaps the job. Profile #1 is the 客服 AI draft. It is not an ai.Engine capability — the Engine is the user-facing code agent, and support is operations work. Why a separate process: its action tools will drive fabric-server’s /admin/* — the same endpoints a human operator uses — so ownership checks, caps and audit logging are enforced once, for humans and AI alike. An in-process agent calling the store directly would bypass every one of those handler checks and force a second, drifting copy of each guardrail. fabric/server/internal/assistant is now just its thin HTTP client.
  • HostBridge real implementation (storage / network / publish)
  • /project/events SSE auto hot-reload of the Preview
  • Mortar BaaS: 5 primitive + 5 feature + 4 SDK + OpenAPI spec

Next slice (in dependency order):

  1. Stronger-isolation project build: the harness runner is a container today → Firecracker / FC
  2. Switch artifacts to object storage (pre-launch) — done: artifact.S3 (Aliyun OSS / MinIO), with the harness and fabric-server pointed at the same bucket
  3. Project → Mortar workspace auto-create wired up: publish_project calls Mortar’s control plane to create a workspace
  4. HostBridge exposes the Mortar SDK to the bundle: injects the tenant id + user token
  5. iOS IAP / domestic Android WeChat Pay integration (see pricing-strategy § implementation complexity)
  6. Subscribe (realtime collaboration) lands (previously deferred)
  7. Self-hosted OTA (post-launch)

Shared invariants

  1. All messages follow fabric/docs/protocol and shared/proto/*.proto (split by module)
  2. Terminology, state machines, enum values follow CONVENTIONS.md
  3. seq, project_id, version are all generated by fabric-server; the Mortar tenant id is generated by Mortar
  4. fabric-server is unaware of user business schemas; Mortar carries its own app_tables / app_columns schema-as-data
  5. The Preview bundle is fully isolated from the host (per-instance Hermes runtime + data-credential, two layers)
  6. The Preview bundle can only hold Mortar-issued tenant tokens; it cannot hold fabric-server session tokens
  7. Artifact bytes are immutable; the version is the only mutable variable
  8. workspace git history IS the AI editing audit log; revert = git reset, no separate undo
  9. Fabric itself does not run on Mortar — fabric-server manages all User and project metadata