Easy Clients: Host and Module

How to design a hand-written single-page client ("Easy client") for an FCS component so that it never falls out of step with the FCS it edits. This page is the design; the hands-on recipe is Build a Component and Its Easy Client.

Status: experimental, proven end to end on 2026-10-07 with the reference component EasyPlank (HiStructOrg/fcs-lab-01, branch deve/easy-plank) on a local platform built from the branch deve/easy-timemachine. Nothing of it is deployed on a shared platform yet. Sections marked proposal are design that has not been built.

1. The problem it solves

Every component is pinned to one commit of its FCS (the prototype string ends with a sha). A configuration created a year ago still runs the FCS of a year ago, while new configurations run today's. The platform's generated editor always fits, because it is generated from the input items of that very commit.

A hand-written client is different: it knows paths, list keys, scene block names and the order of writes of one version of the FCS. Served as one bundle for all configurations, it is either wrong for the old ones or it freezes the FCS. HBC Easy is in exactly that situation.

The fix: the FCS-dependent part of the client becomes part of the commit. The component's existing pin then selects the matching client — a time machine for the client, the same way the platform already has one for the FCS.

2. The split: host and module

A client is cut into two parts along one question: does this code have to change when the FCS changes?

Host (constant tier) Module (FCS-dependent tier)
Deployed once, on a server, like any web app; always the latest published into the content store; named by the FCS commit
Pinned? no — it reads the pin yes — through ClientReference in that commit's Definition.fcscdm
Knows the FCS? no paths, no identifiers, no list keys yes — this is where all of that lives
Examples logo and frame, first impression / progress, session switcher, "new configuration" button, sign-in, asking the platform which client a component needs, loading and mounting it, error pages input panels, labels and units for inputs, the 3D viewer and what scene blocks mean, outputs shown, product texts, the order of writes, validation messages
Changes when the platform or the brand changes the product changes
Who writes it platform (or the product team, once, for its brand) the product author, in the same pull request as the FCS

Two rules decide borderline cases:

  1. If it must run before you know which commit you are on, it is host. The host is the thing that finds out the commit, so it cannot be pinned to one.
  2. If it would break when an input is renamed, added or removed, it is module. A dropdown for input.inEndShape is module code even if it looks generic.

Keep the host thin. Everything you move into it becomes a promise to every module version, old and new.

3. How it works at runtime

host                         platform                          content store
 │  open component 490067       │                                  │
 ├── GET Model/GetComponentClient?id=…&privateKey=… ─►│            │
 │◄── {Prototype: "…EasyPlank.fcc*f7bbe19…",          │            │
 │     ClientReference: "sha256:db7eacb9…"} ──────────┤            │
 ├── GET /cas/manifests/db7eacb9….json ──────────────────────────► │
 │◄── {hostApi:"1", entry:"assets/main-CQ60XCRm.js", css:[…]} ──── │
 ├── import("/cas/files/assets/main-CQ60XCRm.js") ───────────────► │  (+ its chunks, e.g. three-…js)
 ├── module.mount(el, ctx)  ── the module now talks to the platform API itself
 │   switch to another component → unmount() → repeat

New configuration. In the experiment the host creates the component from the palette's branch reference and then asks which client it got — the new component's own pin decides, never the palette. Proposal: resolve the palette entry to an absolute sha first and create the component at exactly that sha, so host, client and component cannot disagree.

4. The contract between them (host API 1)

The module is an ES module that exports three things:

export const hostApi = "1";
export function mount(el: HTMLElement, ctx: EasyContext): void | Promise<void>;
export function unmount(): void;

and receives:

interface EasyContext {
  hostApi: "1";
  apiBase: string;        // prefix for platform calls ("" = same origin); never ends with "/"
  space: string;          // e.g. "deve-rv"
  lang: string;           // for the Content-Language header, e.g. "en"
  componentId: string;
  privateKey: string;
  prototype: string;      // the absolute prototype string the component is pinned to
  onStatus?(text: string): void;
}

Rules, all learned while building EasyPlank:

Versioning. The contract has its own version, hostApi, declared by both the module and its store manifest. A host refuses a module that needs a host API it does not speak. Because old modules live forever, the host may only ever add to the contract; a breaking change means a new host API version that the host serves in addition to the old one. The platform API the module calls is a second contract with the same obligation: what a published module calls must keep working.

The authoritative text is CONTRACT.md in HiStructOrg/easy-plank.

5. The developer's workflows

Your freedom is unchanged: any framework, any bundler, any library (EasyPlank uses Vite, TypeScript and three.js). The only requirements are on the output: ES modules, the entry's exports preserved, a build manifest, relative chunk imports (base: './').

  1. Local loop. npm run dev in the module folder runs a small harness page that mounts the module against a real test component (Vite proxies /api to the platform). Hot reload, no publishing.
  2. Release. Build → publish-client copies the output into the store and prints the address → put "ClientReference": "sha256:…" into Definition.fcscdm → commit it together with the FCS change it belongs to → push. Publish first, reference second: a commit may only name a client that already exists in the store. Builds are deterministic (verified across machines, Node versions and Windows/Linux once line endings are normalised with .gitattributes), so republishing the same source gives the same address.

A commit that changes only FCS keeps the previous ClientReference — legitimate when the change does not affect the client (EasyPlank's colour fix), or a deliberate intermediate step.

6. Guidelines for module authors

7. Where this is going (proposal)

The experiment proves the host/module cut. The fuller design splits further — summarised here so the direction is visible:

What exists today