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
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Publisher-prefixed, lower snake-case (e.g. par_signature_pad). Bound to a form field via the field's control property. |
displayName | string | yes | Human-readable name shown in the form designer's field palette. |
description | string | null | no | Short description shown alongside the control. |
scope | string | yes | The Module Federation container's global scope name (unique per remote), read from the build's mf-manifest.json rather than retyped. |
module | string | yes | The exposed module path the Player loads (e.g. ./Control). Must start with ./. Also read from the build. |
entryFileName | string | no | The 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. |
packageHash | string | null | no | Lower-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. |
remoteEntryUrl | string | null | no | Hosted 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. |
substitutesFor | string[] | yes | The 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@1and enforces thatlogicalNamestarts 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.
Related
- Artifact Package Manifest — the
parsware.jsoninside the archive this manifest is composed from. - Custom App Manifest — the Custom React App analogue, which has not yet adopted packaging.
- Model-Driven Form Definition — a form field's
controlproperty references a control by itslogicalName.