Farther ShoreDocs
Go to Farther Shore
The fs.business() program
Meters & measures
Counted resources
Routes & access groups
Route groups & grants
Plans & pricing
The build output
Team RBAC
Role ownership and defaultsTwo independent checksSubject choiceEffective grants and key typesCustom permission subjectsComponent access panelOperate customer roles
Tenancy & identity
Frontend integrations
@farthershore/business
Add metered routes
Add a resource limit
Add team RBAC
@farthershore/business exports
@farthershore/business/codegen exports
Status
Docs/Build your product/Team RBAC

Team RBAC

Enable customer-managed roles over the route permission catalog.

Managed RBAC adds a route-permission check for credentials inside a subscriber organization, including organization API keys, not only member credentials. RBAC enablement is platform-owned: turn it on once for the business in the dashboard (the Access control (RBAC) card in business settings) or from the CLI, and declare routes normally:

bash
farthershore business rbac enable   # or: disable
farthershore business rbac          # read the current setting

Effective enforcement requires both the business flag and each subscriber organization's own RBAC setting to be enabled. If either flag is off, credential permission resolution can return ['*'], even when a key retains restrictive bindings. Do not use a disabled flag to test least-privilege access.

The subscriber flag has two owners. The subscribing organization turns it on from its portal's Settings → Team page (/settings/team, linked in the portal nav). You can also turn it on — and read or repair its roles — from the builder plane:

bash
farthershore consumer list <business>                 # find the subscriber id
farthershore consumer rbac enable <business> <subscriberId> \
  --default-role reader                               # or: disable
farthershore consumer rbac roles list <business> <subscriberId>

The product flag comes first: with business rbac off, every per-subscriber call answers 400 RBAC_NOT_ENABLED_BY_PRODUCT. If the subscriber organization has mandatory change control on, a direct enable/disable answers 409 GOVERNED_BY_CHANGE_SET and the change must go through its governed ChangeSet instead.

The business flag applies to every environment and takes effect at the edge automatically, with no republish of the business. Disabling removes the managed member-role restriction across all environments; members still face plan, subject, and other gateway checks. Role configuration is preserved for re-enable. Treat disabling as an access expansion, not a routine repair for a single denied member.

ts
import * as fs from "@farthershore/business";

fs.backend("api");
const reports = fs.route("/v1/reports", {
  get: {},
  post: {},
});
const requests = fs.requests();

fs.plan("team", {
  kind: fs.plan.kind.flat,
  price: fs.money.usd(49).monthly(),
  grants: [reports],
  limits: [requests.perMinute(600)],
});

export default fs.business();

The platform derives the grantable permission catalog from compiled route operations. Customer organization owners configure roles and assign members at runtime; those assignments are not business contract source.

Role ownership and defaults

The Business SDK and compiler produce a raw permission catalog. They never produce customer roles. Enabling RBAC also creates no roles and chooses no default. Each subscribing organization uses its customer access-control surface to:

  1. create one or more CUSTOM product roles from the current catalog;
  2. choose a default role, or deliberately leave the default unset;
  3. assign roles and optional direct grants to its members and credentials.

As the builder you can drive the same rows for support — farthershore consumer rbac roles create|update|delete and farthershore consumer rbac assign — over one shared service layer, so a change made from either plane is the same change. --permissions is set-replace: the list you pass becomes the role's whole grant. The reserved key owner can never be created, edited, or deleted (409 OWNER_ROLE_IMMUTABLE): owners always hold every permission. A permission outside the derived catalog is rejected with 400 UNKNOWN_PERMISSION.

With no explicit role and no configured default, a nonowner receives no product permissions. There is no platform Member or Admin product-role template to fall back to. This makes the subscriber organization's choices authoritative and prevents a newly declared SDK permission from silently entering an existing role.

Account roles (owner, admin, and member — the canonical lowercase vocabulary every API, CLI, and MCP surface accepts; OWNER/ADMIN/VIEWER are the stored values, with VIEWER shown as member) are a separate permission plane for subscriber-workspace administration. The subscribing organization governs who holds those account roles. Product-role bindings resolve CUSTOM roles only; passing an account-role key as a product role is rejected. The account owner remains the explicit full-access authority, so owner success never proves that a nonowner product role is correct.

Product roles are scoped to one subscribing organization within one managed business. They are not authored by the builder and are not separate catalogs per deployment environment; an ephemeral environment selects test identities and entitlements, while the subscriber's role definitions remain subscriber governance state.

Two independent checks

Plan access and credential permission both have to pass:

  1. the subscriber's plan must grant the route;
  2. the calling credential's effective permissions must allow the route operation.

RBAC cannot widen a plan. A role that names a route absent from the subscriber's plan does not make it callable.

Subject choice

RBAC also constrains organization keys. Use requireMember: true when an operation specifically needs member identity: permission to call a route is not proof that the credential represents a person. Plan entitlement, member identity, and permission are separate checks. Backend tenant and record ownership checks remain necessary even after all gateway checks pass.

Effective grants and key types

With both RBAC flags enabled:

Caller or bindingEffective product permissions
Organization owner*; owner success does not prove a nonowner role works
Member with explicit product rolesUnion of those roles plus direct grants
Member with no explicit role keysConfigured default role, if any, plus direct grants
Member with only stale/deleted explicit role keysNo fallback to default; only any direct grants remain
Ordinary API key with no roles and no restrictions*, not deny-all
Ordinary API key with role bindingsUnion of the current CUSTOM role grants; stale keys contribute nothing
API key with nonempty restrictionsWildcard-aware intersection of role grants and restrictions; without roles, restrictions narrow full access
SERVICE credentialFrozen service-account grant snapshot, not a live role binding; an empty snapshot denies all

An empty ordinary-key restriction list means no additional restriction, not deny-all. Role unions are additive: a narrower second role cannot subtract access granted by another role. Narrow the granting role or credential restriction.

Role edits republish live-bound key claims and member authorization overlays. This propagation is asynchronous; test existing credentials after the change has reached the edge, not just newly issued credentials. SERVICE snapshots have different semantics and must not be assumed to track later role edits.

Custom permission subjects

Route-derived and managed permissions cover API calls and platform surfaces. For your product's own domain vocabulary (for example a reports subject with generate and purge verbs), declare a permission group in the business program — a route group that carries a permission block:

ts
import * as fs from "@farthershore/business";

fs.backend("api");
const list = fs.route("/v1/reports", { get: {} });
const generate = fs.route("/v1/reports/generate", {
  post: { permission: "generate" },
});
const requests = fs.requests();

fs.group("reports", [list, generate], {
  permission: {
    verbs: ["generate", "purge"], // extra domain verbs beyond read/write
    escalatory: ["purge"], // excluded from write-class expansion
    description: "Report management",
  },
});

fs.plan("team", {
  kind: fs.plan.kind.flat,
  price: fs.money.usd(49).monthly(),
  grants: [list, generate],
  limits: [requests.perMinute(600)],
});

export default fs.business();

The group id becomes the permission subject. Member routes are gated at the gateway by <subject>:read / <subject>:write (derived from the HTTP method), replacing their per-route subjects; metering and grant identity are untouched. An operation may override the derived verb with permission: "<verb>" — the gateway then requires <subject>:<verb> exactly. Custom permissions are grant-by-exact-name only: <subject>:* is never grantable for a custom subject, and escalatory verbs never enter any write-class expansion.

GET, HEAD, and OPTIONS derive read; other methods derive write. At runtime reports:write does not imply reports:read or reports:generate. Give a reader/writer both exact read and write grants. An operation override must name an extra verb declared by its containing permission group; permission: "read" and permission: "write" are invalid overrides. Omit them to use the method-derived default.

The rules: read and write may not be re-declared as extra verbs (the group's route gating implies them); a route belongs to at most one permission group; permission groups may not nest inside each other (plain groups may nest); names and verbs are lowercase snake (2–32 chars, starting with a letter) and the subject may not collide with a managed or platform subject, custom, or any route id. A permission group needs no plan grant to be declared — but as always, RBAC cannot widen a plan.

The platform syncs this business-wide vocabulary on every apply, including environment-scoped preview applies. A preview permission change is not isolated from production vocabulary. Deleting a permission group strips its grants from every role and API key and republishes edge claims; shrinking the verb list strips the removed strings the same way. In the grantable catalog the group's read/write pair appears with the route-derived permissions and extra verbs appear in the custom group. No existing or future subscriber role receives them automatically; a subscriber owner or admin grants every permission by explicit role composition or direct assignment.

Treat group deletion, rename, verb removal, and moving an existing route into a permission group as permission migrations. Grouping replaces the route's former permission subject, so old per-route grants no longer satisfy the new subject. Inspect affected roles, keys, and UI gates across environments before applying; reintroducing a deleted subject does not reconstruct grants that were removed.

The listing surfaces are read-only: the dashboard shows the synced subjects, and so does the CLI —

bash
farthershore business rbac subjects        # read-only list of synced subjects

The API mutation endpoints (PUT/DELETE /businesses/:id/rbac/subjects/*) are gone; the business program is the only author.

To gate your portal UI on a custom subject — and let subscriber orgs configure that gate like the managed components — register a custom:<slug> component id: see Custom components.

The strings flow end to end: subscriber roles grant them, they ride the signed context claim, and the gateway enforces them at the boundary. Because the extra verbs ride the claim, your backend may additionally check them with the same requirePermission(ctx, "reports:generate") it uses for any other permission — defense in depth on top of the gateway, never a substitute for it.

Use hasPermission or requirePermission only on a verified backend request context. Missing permission claims deny in these carrier helpers. Do not use the lower-level permissionSatisfies predicate as a request authorization boundary: its raw missing-claim behavior differs. Require concrete permission keys; requiring reports:* is not a way to require every reports verb.

Component access panel

FsComponentAccessPanel (from the frontend SDK's component kit) is the org-admin surface for per-subscriber component gate policies. It lists every managed SDK component plus any custom:<slug> component with an existing policy, and lets an admin override each component's required permission (picker fed by the same derived permission catalog the role editor uses) and its gate mode (hide, disable, readOnly, or denied).

The panel self-gates on the subscriber team:manage_rbac permission (overridable via the canManage prop) — non-managers render nothing. These policies drive the SDK's client-side component gating only; the gateway's permission check remains the security boundary.

Operate customer roles

The business program owns which routes exist; whether managed RBAC is enabled is a platform-owned business setting (dashboard, CLI, or PUT /businesses/:id/rbac), and the per-subscriber enforcement flag is operational state you can also drive (farthershore consumer rbac enable|disable, PUT /businesses/:id/consumers/:subscriberId/rbac). Customer team membership, role definitions, and assignments are platform-owned operational state. See Customer operations.

Before removing or renaming a route, inspect its active dependents so an existing role or frontend permission gate is not silently stranded.

PreviousThe build outputNextTenancy & identity

On this page

Role ownership and defaultsTwo independent checksSubject choiceEffective grants and key typesCustom permission subjectsComponent access panelOperate customer roles