Farther ShoreDocs
Go to Farther Shore
Frontend SDK
Root & data components
Auth & sessions
Access-aware UI
Permission gates
Custom components
1. Declare the permission vocabulary2. Register the component id3. Gate the component4. Subscriber organizations configure itHow the pieces travel
Variables
@farthershore/farthershore-js
Share with another member
Shared and private in one portal
@farthershore/farthershore-js exports
@farthershore/farthershore-js/react exports
@farthershore/farthershore-js/test-utils exports
@farthershore/farthershore-js/errors exports
@farthershore/farthershore-js/components exports
@farthershore/farthershore-js/components/docs-chrome exports
frontend-sdk HTTP contracts
Status
Docs/Build the customer UI/Custom components

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):

ts
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):

tsx
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:reports before 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 pinned permission/gateMode would 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

tsx
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.

PreviousPermission gatesNextVariables

On this page

1. Declare the permission vocabulary2. Register the component id3. Gate the component4. Subscriber organizations configure itHow the pieces travel