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-typesfor 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 —
- Keel doesn’t target the web, so Keel has no Hosting layer (see
keel/docs/architecture.md)- Appunvs is broader than Keel and has to handle Web / Desktop / WeChat project publishing, so Appunvs runs its own Hosting infrastructure (see
#appunvs-self-run-hosting-infrastructurebelow)
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
| Object | Where it lives | Notes |
|---|---|---|
| Project | app_projects table | The user-owned project unit; state = draft | published | archived |
| Workspace | Bare git repo on fabric-server disk (workspace/<project_id>/) | Every AI fs_write → one git commit; the build reads the HEAD snapshot |
| Bundle | app_bundles table + object storage | (project_id, version) unique; content-addressed sha256; immutable |
| AI Turn | ai_turns table | One 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>/*pathresolves 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 toindex.htmlfor 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:
- 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
- Each KeelView carries its own
- 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
| Interface | v1 | v2+ |
|---|---|---|
cloudbuild.Builder | HarnessBuilder (the only implementation, parameterized by project_type = web | mobile | desktop) | Harden the harness-side runner isolation (container → Firecracker / FC) |
artifact.Store | S3 (S3-compatible: Aliyun OSS in prod/staging, MinIO locally; LocalFS is unit-tests only) | AWS S3 / Cloudflare R2 multi-cloud |
ai.Engine | StubEngine (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
| Capability | Owner | Where it lives |
|---|---|---|
| User account / device / session JWT | Appunvs fabric-server | fabric-server SQLite/Postgres, see auth |
AI conversation engine (/ai/turn, ai_turns table) | Appunvs fabric-server | fabric/server/internal/ai |
Project metadata / state machine (app_projects) | Appunvs fabric-server | fabric-server DB |
| Project workspace git repo | Appunvs fabric-server | fabric-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 storage | written by fabric-harness, read by fabric-server | S3-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-server | Redis pub/sub |
| AppUser account / device / JWT (end-user of every published app) | Mortar feature/auth | Mortar Postgres, see mortar/docs/auth.md |
Data reads/writes inside the AI bundle (mortar.from(...)) | Mortar feature/data | Mortar Postgres |
| AI bundle file upload / download | Mortar feature/files | Mortar OSS |
| AI bundle SSE realtime subscription | Mortar feature/realtime | Mortar SSE broker |
| User-defined cloud function (HTTP-callable) | Mortar feature/compute | Mortar FC |
| Mortar usage / credit ledger | Mortar feature/usage | Mortar 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
| Boundary | Protocol | Client |
|---|---|---|
| User host shell → fabric-server | HTTP + SSE-based events via /project/events | iOS / Android host |
| AI bundle → Mortar Cloud | Mortar HTTP API + derived user token | Inside 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 key | fabric-server |
| Mortar → Aliyun RDS / OSS / Tair / FC | Each cloud’s native SDK | Mortar 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:
| Service | Provider | Use |
|---|---|---|
| Build | KAS Build | metro + Hermes bytecode → ipa / apk |
| Update | KAS Update | manifest + bsdiff CDN distribution → host shell + Keel Go |
| Submit | KAS Submit | App Store + 5 top Chinese Android store submission |
| BaaS | Mortar | AppUser 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
| Service | Content | Status |
|---|---|---|
| Web Build | vite 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 Hosting | The 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 it | Web |
| HTTPS cert | Let’s Encrypt auto-issue + renewal (when user binds a custom domain) | Web |
| Mortar | Shares 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
| Service | Content | Status |
|---|---|---|
| Desktop Build | electron-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 runner | Desktop |
| Binary Hosting | The 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 editor | Desktop |
| Auto-Updater Endpoint | electron-updater-protocol-compatible (https://updates.appunvs.com/<projectid>/...); the user’s Electron app checks on startup | Desktop |
| Code Signing | macOS notarization (Apple Developer ID) + Windows EV cert | Desktop+ |
Analogy: A hosted Squirrel.Mac / electron-builder + electron-updater.
WeChat Mini Program — WeChat’s own infrastructure
| Service | Provider | Use |
|---|---|---|
| Build | miniprogram-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 |
| Distribution | WeChat Open Platform (NOT Appunvs self-run) | Trial submission / review / release |
| Domain | WeChat-owned | Mini 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)
| Service | Use |
|---|---|
| fabric-server | User account / device auth / AI agent / Project workspace (git) / SSE-based events via /project/events / billing |
| Mortar BaaS | The 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-goSourcemap + 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,workspaceinterfaces + v1 implementationsfabric/server/internal/store/turns(theai_turnstable)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 carrythinking/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 anai.Enginecapability — 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/assistantis now just its thin HTTP client.- HostBridge real implementation (storage / network / publish)
/project/eventsSSE auto hot-reload of the Preview- Mortar BaaS: 5 primitive + 5 feature + 4 SDK + OpenAPI spec
Next slice (in dependency order):
- Stronger-isolation project build: the harness runner is a container today → Firecracker / FC
Switch artifacts to object storage (pre-launch)— done:artifact.S3(Aliyun OSS / MinIO), with the harness and fabric-server pointed at the same bucket- Project → Mortar workspace auto-create wired up: publish_project calls Mortar’s control plane to create a workspace
- HostBridge exposes the Mortar SDK to the bundle: injects the tenant id + user token
- iOS IAP / domestic Android WeChat Pay integration (see
pricing-strategy§ implementation complexity) - Subscribe (realtime collaboration) lands (previously deferred)
- Self-hosted OTA (post-launch)
Shared invariants
- All messages follow fabric/docs/protocol and
shared/proto/*.proto(split by module) - Terminology, state machines, enum values follow CONVENTIONS.md
seq,project_id,versionare all generated by fabric-server; the Mortar tenant id is generated by Mortar- fabric-server is unaware of user business schemas; Mortar carries its own
app_tables/app_columnsschema-as-data - The Preview bundle is fully isolated from the host (per-instance Hermes runtime + data-credential, two layers)
- The Preview bundle can only hold Mortar-issued tenant tokens; it cannot hold fabric-server session tokens
- Artifact bytes are immutable; the version is the only mutable variable
- workspace git history IS the AI editing audit log; revert =
git reset, no separate undo - Fabric itself does not run on Mortar — fabric-server manages all User and project metadata