WeChat Mini Program — architecture spec
Status (2026-07): GA-ready on the code side. WeChat is its own harness authoring type (WXML + TypeScript prompt + JSON/page/tsc check_build + seeded scaffold); miniprogram-ci runs IN the harness (
runner-wechatimage’swechat-ci.js, mode=upload|preview) driven bycloudbuild.HarnessWeChatBuilder(the retired server-sideWeChatMPDockerBuilder+cloud-build-wechatare gone). Publish (upload), dev-version preview (QR), appid/.key onboarding via the unified Publish Config, and test-connectivity all ship across web/iOS/Android. Review stays manual (self-managed model — see W5). The one non-code GA prerequisite is ICP filing for the Mortar API domain.
WeChat MP is the fourth pipeline (see project-types.md).
Primary market: Chinese ToB商家 (餐饮/零售/O2O) — they
overwhelmingly prefer WeChat Mini Program (小程序) to native apps.
WeChat MP is the non-negotiable Chinese platform for these use cases. Skipping it = forfeiting half of Chinese ToB revenue.
Why native WXML + TypeScript (not Taro / uni-app)
Taro and uni-app compile React/Vue to WXML. AI builders don’t need “write once compile many” — AI can generate WXML directly. Taro’s compile pass is lossy; WeChat MP training data is dominated by raw WXML in the wild (Tencent’s own docs are WXML). Generating native WXML straight from the AI prompt produces higher-quality output than going through a translation layer.
Page logic is authored in TypeScript (.ts, not .js):
miniprogram-ci compiles .ts → .js via its typescript plugin
(useCompilerPlugins: ["typescript"]), the ambient App/Page/wx.*
types come from miniprogram-api-typings, and check_build runs a real
tsc --noEmit — so the AI gets type feedback WXML/WXSS can’t give.
This is the same “a dedicated prompt per platform” reasoning that drives the Web / Desktop architectures.
Architecture — everything runs in the harness
The whole WeChat pipeline lives in the harness, symmetric with Web/Mobile.
fabric-server has no Node / miniprogram-ci; the project source is a git
worktree the harness owns; so both compile (check_build’s tsc) and
publish/preview (miniprogram-ci) run inside the project’s runner-wechat
sandbox. fabric-server only ships the decrypted appid + upload key over the
internal API when a publish/preview fires.
Chat turn
│
▼ AI emits WXML / WXSS / TS via fs_write
┌──────────────────────────────────────────────────────────┐
│ harness worktree (/workspace) │
│ app.json · app.wxss · app.ts · pages/<p>/<p>.{wxml,wxss, │
│ ts,json} · project.config.json (appid=REPLACE_BEFORE_ │
│ UPLOAD sentinel) · tsconfig.json · package.json │
└──────────────────────────────────────────────────────────┘
│ │
│ check_build │ publish_project / wechat_preview
▼ (runner-wechat) ▼
pure-Go structural checks fabric-server loads KMS creds,
(app.json pages · per-page .wxml constructs cloudbuild.Harness-
+ .ts/.js · *.json valid) + tsc WeChatBuilder, POSTs appid+key
(ensureWeChatTSDeps → runTSC) to the harness internal API
│ │
└───────────── same runner-wechat ◄──────┘
│ POST /v1/projects/{id}/wechat/publish {mode}
▼ preview.Manager.WeChatCI(mode, key)
node /opt/wechat/wechat-ci.js (key → /tmp, scrubbed after)
│ ci.upload OR ci.preview
▼
微信开放平台 (mp.weixin.qq.com)
│
upload ──► 开发版 → user submits for review → releases (manual, 公众平台)
preview ──► 开发版二维码 (scan on a real device, no publish)
Key material discipline: the .key is written to /tmp/fb-wechat-key inside
the runner (never the /workspace worktree, so the commit-back’s git add -A
can’t pick it up) and scrubbed (zeroed + removed) after the run. The appid is
passed as an env var to wechat-ci.js, which hands it explicitly to
ci.Project({appid}) — so the in-repo REPLACE_BEFORE_UPLOAD sentinel is never
rewritten and can’t accidentally publish to a real account from a stray build.
Components (what’s built)
| Concern | Component | Location |
|---|---|---|
| Wire enum | RUNTIME_KIND_WECHAT_MINIPROGRAM | shared/proto/project.proto |
| Authoring type | types.ProjectTypeWechat; projectTypeFor() maps the RuntimeKind → "wechat" | fabric/harness/pkg/types, fabric/server/internal/ai/harness_engine.go |
| AI prompt (W4) | prompt.WeChatInstructions (WXML/WXSS/TypeScript; forbids React/DOM) | fabric/harness/internal/prompt/builder.go |
| Scaffold seed | templates/wechatapp/ (TS starter: app.ts, pages/index, tsconfig, package.json, project.config.json) seeded at Create() | fabric/server/templates/wechatapp/ |
| check_build | pure-Go runWeChatMPChecks (page registration · per-page files · JSON validity) + tsc (ensureWeChatTSDeps → runTSC) | fabric/harness/internal/tools/check_build.go |
| Runner image | fabric/runner-wechat:latest — Node + miniprogram-ci baked at /opt/wechat; ImageFor(ProjectTypeWechat) routes here | fabric/harness/runner-images/wechat/ (Dockerfile + wechat-ci.js + package.json) |
| CI shim | wechat-ci.js — one mode-dispatched script (WECHAT_MODE=upload|preview) → ci.upload / ci.preview | fabric/harness/runner-images/wechat/wechat-ci.js |
| Harness runner | preview.Manager.WeChatCI(projectID, appid, version, desc, mode, key) — stages key, execs wechat-ci.js, scrubs | fabric/harness/internal/preview/wechat.go |
| Harness API | POST /v1/projects/{id}/wechat/publish (Bearer internal secret) | fabric/harness/internal/api/wechat.go |
| Server builder | cloudbuild.HarnessWeChatBuilder — implements the WeChatBuilder interface: Publish (upload → WeChatResult{ResultJSON, Log}) + Preview (dev QR); constructed per-publish, bound to the project’s own creds. Deliberately separate from cloudbuild.Builder/HarnessBuilder, which returns an artifact.Manifest | fabric/server/internal/cloudbuild/harness_wechat.go |
Authoring — the WeChat project type
A WeChat project is its own harness ProjectType (NOT web-in-disguise): WXML is a
genuinely different source language, so it gets its own prompt and its own
check_build. There is no dev server (a Mini Program can’t render in a
browser iframe) — “preview” is a dev-version QR (below).
- Prompt:
WeChatInstructionsteaches WXML components (<view>/<text>, not<div>), directives (wx:if/wx:for),Page({data, ...})+setData, thewx.*APIs, TypeScript authoring, and theREPLACE_BEFORE_UPLOADappid sentinel. It explicitly forbids React/DOM/Tailwind andrun_dev_server. - Scaffold:
Create()seedstemplates/wechatapp/(the WXML twin oftemplates/webapp/) so a fresh project opens buildable instead of an empty worktree that fails turn-1 check_build. - check_build: structural validation in pure Go (every
*.jsonparses,app.jsonregisters ≥1 page, each page has.wxml+.ts/.js,project.config.jsonpresent) plustsc --noEmitwhen atsconfig.jsonis present —ensureWeChatTSDepsinstallstypescript+miniprogram-api-typingsin the runner, then reuses the sharedrunTSC.
Credentials + onboarding — the unified Publish Config
appid + .key onboarding is not a bespoke WeChat form — it is one field set
in the cross-runtime Publish Config (internal/publishconfig), the same
surface that carries web custom-domain / iOS bundle+signing / Android
package+keystore. See the publish-config layer for the full model; the WeChat
slice is:
appid— an identifier (public-ish), stored on the project row (wechat_appid), validated^wx[0-9a-f]{16}$.upload_key— a secret file (the.key), KMS-encrypted intoapp_project_secrets(kindwechat_mp_key) via the secrets store; write-only (the bytes never return — status isconfigured ✓only).- Inline SOP ships with the schema: register on 公众平台 → copy appid →
download the upload key → add the build egress IP to the IP allowlist (the
#1 user failure) → upload the
.key. - API:
GET/PUT /project/:id/publish-config(runtime-shaped schema + status; identifiers echo their value, secrets never do). Localized server-side (Describe(rt, lang)offAccept-Language), rendered on web/iOS/Android. - test-connectivity:
POST /project/:id/publish-config/test— for WeChat it runs a realci.preview(the only way to validate appid + key + IP allowlist against Tencent), returning{ok, message}. A “Test connection” button sits in the Publish Config form on all three clients.
Publish + preview
Both run the same runner-wechat path, differing only by WECHAT_MODE:
- Publish (
publish_project):HarnessWeChatBuilder.PublishPOSTsmode:"upload"→ci.upload→upload-result.json, returned ascloudbuild.WeChatResult{ResultJSON, Log}; its 体验版二维码 URL becomesBundle.URI. WeChat is the one publish path with no stored manifest — the other three (web / mobile / desktop) have the harness upload every output file content-addressed and hand back anartifact.Manifest, but here Tencent IS the artifact host, so there is nothing of ours to store or serve.Bundle.Manifeststays NULL and no/web-previewroute applies. - Preview (
wechat_preview):HarnessWeChatBuilder.PreviewPOSTsmode:"preview"→ci.preview→ a 开发版二维码 (base64 PNG). NOT a publish — no 体验版 slot, no review — but it needs the SAME real appid + key (miniprogram-ci signs a Tencent round-trip either way), soPOST /project/:id/wechat-previewis gated onHasWeChatCredentials. Each client decodes the base64 → a scannable QR in the preview surface.
W5. Audit + review — done (self-managed model)
Status: complete for the self-managed model.
- Audit log — done. Publish emits
audit.Event{kind:"wechat_mp_publish"}(AuditKindWeChatMPPublish) and the dev-version preview emitswechat_mp_preview(AuditKindWeChatMPPreview), so the per-user audit trail records every Tencent upload / preview. - Review status / rejection — NOT automated (by design). Submit-for-review,
audit-status query, and release are the wxa openapi code-management
endpoints (
wxa/submit_audit,wxa/get_latest_auditstatus,wxa/release), which are 第三方平台 (open-platform third-party) APIs. A self-managed Mini Program — which is exactly what Fabric’s miniprogram-ci + upload-key path is — cannot call them with its own token. So review is done manually in the 公众平台 版本管理 page, and Fabric links to it from the preview surface on all three clients (web / iOS / Android).
Why Fabric will NOT become a 第三方平台 (decision, 2026-07)
Automating review would require registering Fabric as a WeChat 开放平台第三方平台. That path is architecturally incompatible with an AI generator — not merely heavy:
- 第三方平台 publishing is
wxa/commit(template_id, ext_json)—template_idpoints at a fixed code package in the platform’s 代码模板库. The model is “one template committed to thousands of merchants, differing only byext_json” — built for template-reseller SaaS (e.g. a 餐饮 SaaS that gives every restaurant the same shell + its own data). - The template library is capped, and templates are added manually via WeChat DevTools; there is no API to commit arbitrary per-merchant source.
- Fabric generates unique source per project. The ONLY WeChat path that pushes arbitrary unique code is self-managed miniprogram-ci + the account’s upload key — precisely what Fabric already uses. Self-managed is the correct shape for a code generator, not a stopgap.
- The two can’t be mixed: once an MP is authorized to a 第三方平台 with the 开发管理 permission, its code is managed there and a self-managed miniprogram-ci upload conflicts.
Conclusion: Fabric stays self-managed permanently; review stays manual; the 第三方平台 route is rejected (model mismatch), not deferred.
Key decisions
- Single appid per project. Two WeChat MPs = two Fabric projects. The project ↔ appid mapping is 1-to-1.
- What’s in git. Only WXML/WXSS/TS source + the
project.config.jsontemplate with theREPLACE_BEFORE_UPLOADsentinel. The real appid +.keyare never committed — appid rides an env var intowechat-ci.js; the key is staged transiently in the runner’s/tmpand scrubbed. - No dev server. WXML can’t render in an iframe and WeChat DevTools is a closed GUI, so “preview” is the dev-version QR (miniprogram-ci preview), not an in-container Vite/metro. This is why WeChat is not runner-web-shaped.
- Alipay / Douyin / Baidu Mini Programs? Deferred. WeChat single-platform first. Each additional platform = its own runner CI shim + credential slot in publish-config. Re-evaluate after WeChat MP ships.
Connection to Mortar BaaS
WeChat MP can hit Mortar Cloud via wx.request. Constraint:
WeChat MP requires HTTPS for any external request, and the domain
must be pre-registered in mp.weixin.qq.com under “服务器域名 →
request 合法域名” (up to 20 domains, must use an ICP-filed domain).
This means Mortar’s prod domain (api.mortar.appunvs.com)
needs ICP filing in China. This is a non-trivial process (2-4
weeks) and must be planned before WeChat MP GA — it is the one
remaining non-code GA prerequisite. Track in mortar/docs/ deployment.md not here.
A mortar/sdk/wechat/ package similar to
mortar/sdk/typescript/ may be useful, exposing wx.request-wrapped
helpers — defer until first WeChat MP project actually needs it.