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:
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:
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.
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:
- create one or more
CUSTOMproduct roles from the current catalog; - choose a default role, or deliberately leave the default unset;
- 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:
- the subscriber's plan must grant the route;
- 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 binding | Effective product permissions |
|---|---|
| Organization owner | *; owner success does not prove a nonowner role works |
| Member with explicit product roles | Union of those roles plus direct grants |
| Member with no explicit role keys | Configured default role, if any, plus direct grants |
| Member with only stale/deleted explicit role keys | No 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 bindings | Union of the current CUSTOM role grants; stale keys contribute nothing |
| API key with nonempty restrictions | Wildcard-aware intersection of role grants and restrictions; without roles, restrictions narrow full access |
| SERVICE credential | Frozen 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:
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 —
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.