Mount a surface from a directory
Serve a built single-page app at a path of its own, such as /crm/, from a
module’s manifest and a directory of files, without rebuilding or
restarting the server.
Prerequisites
Section titled “Prerequisites”- A running Henceforth server that you started with
--metadata-path, and its URL. - A built surface: a directory with an entry file (for example
index.html) and its hashed files underassets/. admin.metadata.applyto apply the manifest, andadmin.metadata.module.readon the module to read the diagnostics in step 7.- The
hfbinary, logged in to the server. See Manage metadata with the hf CLI.
1. Build the surface
Section titled “1. Build the surface”Run your surface’s own build. The platform does not build it: it serves the
output as is. The steps use a module crm, a build output directory dist,
and the entry file index.html.
2. Place the files under the surfaces directory
Section titled “2. Place the files under the surfaces directory”Put the build output at <directory>/<moduleKey>/<assets>/, where
moduleKey is the module’s qualified key (crm, or acme.crm for a module
with a vendor) and assets is the directory name you will declare in
step 4:
metadata/ surfaces/ crm/ dist/ index.html assets/ app-3f9a1c.js3. Set the surfaces directory
Section titled “3. Set the surfaces directory”In settings.json5, in the metadata directory, name the directory from
step 2 with a relative path:
{ surfaces: { directory: "surfaces" },}A relative path resolves against the metadata directory, so the same value
works under hf dev and in production. The server reads this setting at
startup: restart it once after adding or changing it. The server refuses to
start if the directory does not exist.
4. Declare the surface and pin the contract
Section titled “4. Declare the surface and pin the contract”In the module’s manifest, crm_module.json5, add a surfaces entry and a
platform block. A module that declares a surface must pin the API contract
version it targets:
{ moduleKey: "crm", label: "CRM", manifestVersion: 1, surfaces: { crm: { mountPath: "/crm/", assets: "dist", entry: "index.html", actions: [ "runtime.data.record.read", "runtime.data.record.query", "admin.metadata.module.read", ], }, }, platform: { apiVersion: "v1" },}actions lists what the surface may do on behalf of the people who use it,
and it decides who sees the surface listed. Declare what your surface calls
and no more: here it may read and query records and read module metadata. A
token issued to the surface is refused everything else. Each entry must be a
known action. The server refuses admin.metadata.apply and every
admin.iam.* action on a module surface. See
Experience surfaces.
5. Validate the metadata
Section titled “5. Validate the metadata”hf validate ./metadatahf validate checks the manifest and the platform pin, and warns for each
surface whose entry file is not where the server will look. It does not
check that the actions entries are known actions; the apply in the
next step does. If surfaces.directory is not a directory, it reports one
warning saying the server will refuse to start, instead of a line per surface.
A warning does not change the exit code.
6. Apply the manifest
Section titled “6. Apply the manifest”hf applyAn unknown actions entry fails the plan and the apply with
APPLY_UNKNOWN_SURFACE_ACTIONS, naming each one. An entry the server will not
let a module surface declare fails with
APPLY_SURFACE_ACTIONS_NEED_APPROVAL, also naming each one. Files and manifest can
arrive in either order: the surface is mounted as soon as it is declared in
the applied metadata and its entry file exists.
7. Check what the server mounted
Section titled “7. Check what the server mounted”curl https://your-server/v1/surfaces \ -H "authorization: Bearer $TOKEN"The surfaces list contains an entry with "name": "crm:crm" and
"mountPath": "/crm/". The name is the module’s qualified key, a colon, then
the surface key, here module crm and surface key crm; the mount path may
change while the name stays. If the surface is declared but not mounted, it is absent from surfaces, and a
caller who can read the module’s metadata sees an unmounted list instead:
{ "unmounted": [ { "name": "crm:crm", "key": "crm", "module": "crm", "mountPath": "/crm/", "reason": "entryMissing", "expectedPath": "/srv/metadata/surfaces/crm/dist/index.html" } ]}Place a file at expectedPath and request the list again; no restart is
needed. The server also logs a warning for each such surface after every
apply.
8. Open the surface
Section titled “8. Open the surface”hf open crm:crmhf opens the surface by the name from step 7, signed in as you. When hf
asks whether to open the surface from module crm, answer y (see
Experience surfaces for why it asks).
In a browser that is not already signed in as you, the page asks
“Sign in as (your name)?”; click Continue. A request for /crm redirects
to /crm/, and a path that matches no file serves the entry page so
client-side routes work. A path segment that begins with . is the
exception: it answers 404 under a directory surface.
Verify
Section titled “Verify”curl -I https://your-server/crm/answers 200 with content-type: text/html; charset=utf-8. A hashed file
such as /crm/assets/app-3f9a1c.js answers 200 with an immutable cache
header, and /crm/assets/missing.js answers 404.
Troubleshooting
Section titled “Troubleshooting”/crm/answers404 NOT_FOUND: the surface is declared but not mounted, or not declared. Readunmountedin step 7.directoryNotConfiguredmeanssettings.json5has nosurfaces.directory, or the server has not been restarted since you added it.- The server exits with
surfaces.directory ... is not a directory: the configured path does not exist. Create it, or correct the setting. - The apply fails with
APPLY_UNKNOWN_SURFACE_ACTIONS: anactionsentry does not name a known action. The error names the surface and the entry. - The apply fails with
APPLY_SURFACE_ACTIONS_NEED_APPROVAL: the surface listsadmin.metadata.applyor anadmin.iam.*action. Remove it fromactions; module surfaces cannot declare these yet. - The surface opens the login form: the browser session expired. Run
hf open crm:crmagain to sign in. hf open crm:crmsays the surface is not mounted or not available to you: it is not in your list. Readunmountedin step 7, or check that you hold one of the surface’s grantable actions.- The login page says the surface “is no longer available”: the surface key
changed, the module was uninstalled, or the surface is not listed for you
(you hold none of its grantable actions). A
mountPathchange never causes this; the browser signs in again silently at the new path. - A call from the surface answers
403 SURFACE_SCOPE_EXCEEDED: the route needs an action the surface does not declare. The message names the surface and the action. Add the action toactionsand apply the manifest; no new sign-in is needed, because the server reads the list on every request. The person must also hold the action: declaring it grants nothing. A route that needs several actions needs all of them, and the message names the first one missing:POST /v1/planneedsadmin.metadata.planand the module, object and field read actions. - A metadata write to
mainanswers403 SURFACE_SCOPE_EXCEEDEDnamingadmin.metadata.apply, although the surface declares the write action itself. A write tomainalso needsadmin.metadata.apply, and a module surface cannot declare it, so adding it toactionsfails the apply withAPPLY_SURFACE_ACTIONS_NEED_APPROVAL. A module surface cannot write metadata tomain, and cannot publish a branch either. It can write to a cart branch (/v1/metadata@{branchId}/...) with the write action alone; a person withadmin.metadata.applypublishes the branch outside the surface, for example withhf. - A call from the surface answers
401 UNKNOWN_SURFACE: the surface key changed or the module was uninstalled. AmountPathchange never causes it. The person signs in again. hf validatewarnsdeclares no actions: the surface can call no action-guarded route. Add the actions it uses. Apply goes further and refuses a module surface with no grantable action (next item), static content included.- Apply fails with
APPLY_SURFACE_NEEDS_GRANTABLE_ACTION: the surface’sactionsis empty, or lists only record or flux actions, which cannot be granted. A surface is listed to a person who holds one of its grantable actions, so add at least one. Choose the narrowest grantable action the surface needs, for exampleruntime.screen.renderfor a surface that only renders screens: every action you declare widens what a token issued to the surface may do, andadmin.metadata.module.readin particular lets it read compiled-module metadata. - Compilation fails with
declares a surface but no platform.apiVersion: add theplatformblock from step 4. - Sign-in shows a list of surfaces instead of opening one, or lands on a
surface you did not expect: the sign-in page opens
henceforth:studiowhen the person can see it, otherwisehenceforth:app, matched by name and only for those two platform surfaces, so a module surface never takes it over, whatever its key or mount path. When neither is listed, the page shows the surfaces the person was listed and opens one only when they choose it; when nothing is listed it says so. CallGET /v1/surfaceswith that person’s token, as in step 7, and readsurfaces: a surface missing from the list is one that is not mounted, or a module surface none of whose grantable actions the person holds.
See also
Section titled “See also”- Experience surfaces – what a surface is, who sees it, and how far it is trusted.
- Module manifest – the
surfacesandplatformproperties. - Settings –
surfaces.directory. - HTTP API reference –
GET /v1/surfaces.