Custom components
Register your own component ids and let subscriber organizations configure their gates exactly like the managed set.
The managed data components self-gate through the component-policy resolver:
each id maps to a render permission, a mutating permission, and a deny
render. A subscriber organization can override the RENDER permission and the
deny render from its access-control panel (the mutating writePermission is
builder/managed-owned — overlays do not carry it). Your own components can
join the same system — one registration makes a builder component
permission-aware, org-configurable, and consistent with everything the SDK
ships.
1. Declare the permission vocabulary
Custom permissions use builder-defined subjects, declared as a permission group in the business program (see Custom permission subjects):
fs.group("reports", [listReports, generateReport], {
permission: { verbs: ["generate"] },
});
Publish the business and the subject syncs on apply. The verbs available for
UI gating are the group's reports:read / reports:write pair (derived from
its routes' HTTP methods) plus any declared extra verbs, such as
reports:generate. Roles grant them like any managed permission — the pair
and every extra verb must be selected explicitly by the subscribing
organization when it composes a role. There is no role seeding and no verb
widening — a role holds exactly the strings it was granted.
2. Register the component id
Component ids are namespaced custom:<slug> — the slug starts with a
lowercase letter followed by 1–31 more of a-z, 0-9, -, _ (2–32
characters total, the same budget as custom permission subjects, so the
slug-derived subject is always declarable). Register the
gate policy once at module scope, in a module that is eagerly imported
during app bootstrap (your entry module, or a component-registry.ts
imported from it):
import { registerComponent } from "@farthershore/farthershore-js/components";
registerComponent({
id: "custom:reports",
writePermission: "reports:write",
});
The resolver derives sensible defaults when fields are omitted — a
custom:reports registration with no permission resolves reports:read
(from the slug), and the deny render defaults to an explicit AccessDenied
panel ("denied").
Registration must run before the first gate resolves. The registry is module-global and NOT reactive: a gate that resolves
custom:reportsbefore the registering module is imported falls back to the derived defaults, and nothing re-renders when the registration arrives later. In a code-split app, do not rely on the lazy page's own import to register its component — a sidebar item or an outer route gate resolves the policy before the chunk loads, and a pinnedpermission/gateModewould not apply. Keep registrations in an eagerly-imported bootstrap module.
Registration precedence matters: a field you SET in registerComponent is
authoritative — the resolver reads registration → subscriber-org override →
defaults, in that order. The example deliberately omits permission and
gateMode so subscriber orgs can re-gate the component and change its deny
rendering from their access panel; register a field only when your product
must pin it.
3. Gate the component
import { withGate } from "@farthershore/farthershore-js/components";
const ReportsPanel = withGate(ReportsPanelImpl, "custom:reports");
withGate resolves registration → subscriber-org override → derived defaults
on every mount. <PermissionGate component="custom:reports">,
useComponentGate("custom:reports"), and
usePermissionAction("custom:reports") (for mutating controls inside the
panel) read the same resolution — see
Permission gates.
4. Subscriber organizations configure it
The access-control panel lists every managed component, plus each
custom:<slug> key that already carries an override row (registrations live
in your bundle, so the first override announces the key to the server — write
it with fs.rbac.componentPolicies.update({ componentKey: "custom:reports", ... }) or the /me/component-policies API). From then on an org admin can
re-gate the component onto a different permission or change how a deny
renders (hide, denied, disable, readOnly); the change reaches every
member's portal through the same /me policy overlay the managed components
use, with a governed audit trail.
How the pieces travel
Registration lives in your bundle; the org's overrides live server-side and
arrive on the authenticated /me document; role grants propagate to the edge
for enforcement. UI gating updates when the member's /me document is
re-fetched — on sign-in, an organization switch, or a page reload. A member
who keeps the portal open sees the OLD gate state until one of those
refreshes happens; edge enforcement updates independently (and first), so a
stale UI never widens what the gateway actually allows.
Component gating is UX. A member can call an endpoint without rendering your component, so the route's permission constraint at the gateway — and your backend's verified context — remain the authorization boundary. Gate the data, then gate the pixels.