Skip to main content

Custom Control Manifest

Schema id: custom-control-manifest@1 Machine-readable schema: custom-control-manifest.schema.json

The canonical description of a Custom Control (PCF-style, Section 6.3): the Module Federation remote the Model-Driven App Player mounts in place of a native field editor inside a form or view. A manifest is validated identically whether it is registered through the Admin Control Center, submitted via a direct API call, generated by an AI assistant, or carried inside a Solution import — the one-validation-path rule (Section 6.7).

It is not written by hand. A control's registration is an upload of the package @parsware/pack built, and this manifest is composed from that archive's own parsware.json (§28). What follows describes the stored shape, for reading an exported solution or validating one you generated.

Shape​

FieldTypeRequiredNotes
logicalNamestringyesPublisher-prefixed, lower snake-case (e.g. par_signature_pad). Bound to a form field via the field's control property.
displayNamestringyesHuman-readable name shown in the form designer's field palette.
descriptionstring | nullnoShort description shown alongside the control.
scopestringyesThe Module Federation container's global scope name (unique per remote), read from the build's mf-manifest.json rather than retyped.
modulestringyesThe exposed module path the Player loads (e.g. ./Control). Must start with ./. Also read from the build.
entryFileNamestringnoThe container entry file inside the package (e.g. remoteEntry.js). The serving URL is computed from the environment, the logical name and this — there is no URL to store. Empty in Hosted mode.
packageHashstring | nullnoLower-case hex SHA-256 of the package archive the control was registered from. Checked against the bytes that travelled in a Solution package on import, so what is served is what was exported. Null in Hosted mode.
remoteEntryUrlstring | nullnoHosted mode only: an absolute http(s) URL to an externally served remoteEntry.js, for hot reload while authoring. Registering one is refused unless the environment's tier is Development, and a Hosted control does not travel in a Solution package — its code is not the platform's to carry.
substitutesForstring[]yesThe field control types this control can be bound to: one or more of Text, MultilineText, Number, Boolean, DateTime, Lookup, File, Image, Custom. Must contain at least one entry. The field palette offers the control only where a field's control type matches. These are the data-type families a field belongs to — not the built-in control names a form placement chooses (TextArea, Toggle, Dropdown, …), which are a different vocabulary that happens to share four names.

What it no longer has​

requestedScopes and declaredEntities are gone, and additionalProperties is false, so a manifest still carrying either is rejected rather than silently ignored. A control runs with the permissions of the person using it (§24): a requested scope set was only ever intersected with the user's own session, so it could narrow a control's user and never widen them, and the declared entity list was read by no code path at all. Deleting both makes the security story one sentence rather than two knobs.

A required remoteEntryUrl is gone too. The bundle now travels as a package served from the platform's own origin, so the URL is computed rather than stored (§28).

The control contract​

The exposed module (./Control) is a single React component implementing the fixed ParswareControlProps contract from @parsware/runtime-sdk:

interface ParswareControlProps {
value: unknown; // the bound field's current value
onChange: (next: unknown) => void; // commit a new value back to the form
fieldMetadata: FieldMetadata; // the bound field's type/label/constraints
context: RuntimeContext; // the same auth/data/logging context apps get
}

The Player supplies these props at render time. A control reaches the platform only through context (a ParswareRuntime) — never a Gateway or Identity call directly.

Where it is validated​

  • Registration — the Application Layer validates the manifest against custom-control-manifest@1 and enforces that logicalName starts with the owning publisher's prefix before recording the control in an environment.
  • Solution import — every embedded manifest in a Solution package is re-validated against the same schema before the control is registered in the target environment.

Security notes​

A Custom Control runs in the same JavaScript realm as the Shell (Module Federation, not an iframe — Section 6.3), so the trust boundary is enforced elsewhere:

  • A packaged control is served from the platform's own origin, so there is no external origin to allow-list. The CSP allow-list still gates Hosted mode, which is the only shape whose bundle a maker can point anywhere — and which is refused outside a development environment.
  • The archive is checked before it is stored: size, file count, how much it expands to, that no entry names a path outside its own folder, and that every file is a type the platform serves.
  • The control is only loadable if a Solution deployed it into that environment, and the package's hash is verified on import and again before it is ever unpacked to disk.
  • Every data call it makes through the Runtime SDK is authorized server-side against the App Context Token, which carries the user's own scopes. A control can do exactly what the person using it can do, and there is no field on this manifest that changes that.