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-wechat image’s wechat-ci.js, mode=upload|preview) driven by cloudbuild.HarnessWeChatBuilder (the retired server-side WeChatMPDockerBuilder + cloud-build-wechat are 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)

ConcernComponentLocation
Wire enumRUNTIME_KIND_WECHAT_MINIPROGRAMshared/proto/project.proto
Authoring typetypes.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 seedtemplates/wechatapp/ (TS starter: app.ts, pages/index, tsconfig, package.json, project.config.json) seeded at Create()fabric/server/templates/wechatapp/
check_buildpure-Go runWeChatMPChecks (page registration · per-page files · JSON validity) + tsc (ensureWeChatTSDepsrunTSC)fabric/harness/internal/tools/check_build.go
Runner imagefabric/runner-wechat:latest — Node + miniprogram-ci baked at /opt/wechat; ImageFor(ProjectTypeWechat) routes herefabric/harness/runner-images/wechat/ (Dockerfile + wechat-ci.js + package.json)
CI shimwechat-ci.js — one mode-dispatched script (WECHAT_MODE=upload|preview) → ci.upload / ci.previewfabric/harness/runner-images/wechat/wechat-ci.js
Harness runnerpreview.Manager.WeChatCI(projectID, appid, version, desc, mode, key) — stages key, execs wechat-ci.js, scrubsfabric/harness/internal/preview/wechat.go
Harness APIPOST /v1/projects/{id}/wechat/publish (Bearer internal secret)fabric/harness/internal/api/wechat.go
Server buildercloudbuild.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.Manifestfabric/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: WeChatInstructions teaches WXML components (<view>/<text>, not <div>), directives (wx:if/wx:for), Page({data, ...}) + setData, the wx.* APIs, TypeScript authoring, and the REPLACE_BEFORE_UPLOAD appid sentinel. It explicitly forbids React/DOM/Tailwind and run_dev_server.
  • Scaffold: Create() seeds templates/wechatapp/ (the WXML twin of templates/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 *.json parses, app.json registers ≥1 page, each page has .wxml + .ts/.js, project.config.json present) plus tsc --noEmit when a tsconfig.json is present — ensureWeChatTSDeps installs typescript + miniprogram-api-typings in the runner, then reuses the shared runTSC.

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 into app_project_secrets (kind wechat_mp_key) via the secrets store; write-only (the bytes never return — status is configured ✓ 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) off Accept-Language), rendered on web/iOS/Android.
  • test-connectivity: POST /project/:id/publish-config/test — for WeChat it runs a real ci.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.Publish POSTs mode:"upload"ci.uploadupload-result.json, returned as cloudbuild.WeChatResult{ResultJSON, Log}; its 体验版二维码 URL becomes Bundle.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 an artifact.Manifest, but here Tencent IS the artifact host, so there is nothing of ours to store or serve. Bundle.Manifest stays NULL and no /web-preview route applies.
  • Preview (wechat_preview): HarnessWeChatBuilder.Preview POSTs mode:"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), so POST /project/:id/wechat-preview is gated on HasWeChatCredentials. 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 emits wechat_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_id points at a fixed code package in the platform’s 代码模板库. The model is “one template committed to thousands of merchants, differing only by ext_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

  1. Single appid per project. Two WeChat MPs = two Fabric projects. The project ↔ appid mapping is 1-to-1.
  2. What’s in git. Only WXML/WXSS/TS source + the project.config.json template with the REPLACE_BEFORE_UPLOAD sentinel. The real appid + .key are never committed — appid rides an env var into wechat-ci.js; the key is staged transiently in the runner’s /tmp and scrubbed.
  3. 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.
  4. 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.