Skip to content

Epic: hotcrm as a single-DB multi-org SaaS (isolated posture) — tenant customization surface + tenant readiness #12701

Description

@os-zhuang

Chartered by maintainer, 2026-08-27, live PM chat, verbatim and untranslated — filed by PM session session_01SVYmuhHW6qZmNBqciaS7BN under the maintainer direct-dispatch channel (standing authorization 2026-08-10):

  • Need: 「我现在的真实需求,就是 hotcrm 这种元数据应用,在单库多组织下能正常使用。」 and 「我想搭一个 hotcrm 的saas 服务,因为是单库,除了内核的表和字段,客户肯定不能改。要评估哪些客户应该可以改。」
  • Posture: isolated — 「1同意」 (hard per-tenant wall, not the group shape).
  • Tenant data: 「2你说的产品目录是什么?每个租户应该各自使用各自的数据吧。」 — every tenant uses its own data, product catalog included.
  • No tenant custom fields: 「我觉得在单库多租户的情况下,可以不考虑让租户做自定义字段」 — single-DB multi-tenant does NOT offer per-tenant fields; operator-added shared field + per-org view overlays is the sanctioned shape when a need is real.
  • Seeds are per-tenant reference data: 「种子也不应该是全局的呀,因为种子数据不同的客户都是要改的呀,只是参考呀,租户要自己删除呀」 — each tenant gets its own editable, deletable copy.
  • Dispatch: 「按照你的建议,创建卡片,并使用项目经理技能派发完成所有相关的开放」, then 「开发」, then 「发版后把 tenant_admin 那格补上创建卡片,现在跑 isolated posture 的真机 dogfood」.
  • Handover, 2026-08-28: 「更新你的 epic 卡片,我让新人接手。 12843 也让新项目经理用 fable 审核」.

🤝 HANDOVER — read this first (2026-08-28)

The chartering PM session stands down here. Everything below is written so an incoming seat needs no context from that chat.

One thing is in flight and it is the only blocker to closing this epic:

Where the evidence lives. The dogfood rig's database is in the chartering session's scratch container and does not survive it. Do not go looking for it — the reproduction recipe (env vars, the three drifted auth walls, the mail-transport stand-in) is written down on objectstack-ai/cloud#1683, and the run record is the 2026-08-27 comment on this epic.


Measured platform baseline (verified on origin/main @ 15bf9e8, 2026-08-27)

  1. NULL-org rows are global rows. The driver compiles tenantId into (organization_id = :tenant OR organization_id IS NULL) (packages/objectql/src/engine.ts:3384) — an org-less business row is visible to every tenant, not hidden.
  2. One platform-wide metadata key. manage_metadata was the only metadata-authoring capability and is scope: 'platform'; tier-A org-overlay writes and env-wide tier-B creates hung off the same key. Closed by Org-scoped presentation customization authority: a tenant org admin authors tier-A overlays without platform-wide manage_metadata #12702.
  3. Per-org seed replay already exists: seed-datasets / seed-replayer (packages/runtime/src/seed-datasets.ts, app-plugin.ts:979-1049) replays the registered dataset union into each newly founded organization, stamped with that org's id; the enterprise @objectstack/organizations plugin invokes it on org creation. What does NOT exist is per-family selection — the replayer always replays the full union, so selection is made at composition time.
  4. ADR-0126 activation ledger is install-level, operator-gated in walled postures (packages/runtime/src/domains/activation-gate.ts) — correct and sufficient for the SaaS operator; per-org disable stays the §5 reserved narrow channel.

Cards — all three chartered cards LANDED

Dogfood run — isolated posture, real machine (2026-08-27), 13/13 PASS

Full run record: the 2026-08-27 comment on this card. Verified live on a walled EE boot (composed artifact, minted enterprise licence, two self-serve tenant organizations): per-tenant catalog replay (13 own-org products each, demo objects empty), cross-tenant isolation with a proven-visible sentinel, tenant /meta write refused 403, tenant flow-toggle refused 403, operator toggle accepted with the durable sys_metadata_activation install-level row retained on re-enable — the first time ADR-0126's §4/§6-wall-3 semantics were observed on a walled deployment (the #12438 D2 environment gap).

Two stand-ins were used and are recorded as such: the artifact copy had the #12497 retired keys stripped (that is #12772), and sys_user.email_verified was set in the DB in place of a mail transport (that is cloud#1683).

Findings the dogfood produced

Card State
#12772 — v17-line artifact compatibility break pm:dispatched, PR #12843 green and awaiting the tier-gated review (see HANDOVER)
#12773 — plugin-auth audience log crash turned 4xx into blind 500s MERGED via PR #12803 (fixed by another seat)
objectstack-ai/cloud#1682 — crm_contract tenant-column false negative fail-closes a hierarchy scope filed, unassigned, awaiting that lane
objectstack-ai/cloud#1683 — dev-local hotcrm-saas rig drifted behind three auth walls (carries the working recipe) filed, unassigned
#12844 — AppPlugin registers the raw bundle's security metadata, bypassing the door conversion (no reachable defect today) finding, awaiting first-touch grading
#12850 — dispatch-gates cannot see a new error/notice code literal, so a green local union ships a red CI finding, awaiting grading
objectstack-ai/hotcrm#1363 — scheduled-sweep gate inspects create_record nodes only finding, awaiting that lane

Follow-ups (carded, unlock conditions written down)

Deliberately not chartered (recorded so silence reads as decided)

  • Per-tenant custom fields (refused by the ruling above).
  • Per-tenant/per-family seed selection (e.g. trial tenants opting into demo data) — uniform catalog-only replay until real pull.
  • Per-org clone of packaged flows (env-wide artifact model stands, ADR-0126 §6.1 wall).
  • A3 org-scoping of schedule / time-relative / api trigger contexts (ADR-0126 §9, unchanged).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions