Root & data components
Mount the SDK once, compose self-managed subscriber surfaces, and keep layout and CSS in your application.
Mount the SDK once, compose self-managed subscriber surfaces, and keep layout and CSS in your application.
The /components entrypoint supplies a managed root plus headless subscriber
components. They fetch through the same client and render stable .fs-* class
hooks, but the package ships no component stylesheet and owns no application
router.
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
ApiKeysPanel,
BillingSummary,
FartherShoreRoot,
FsSignIn,
PlansTable,
SignedIn,
SignedOut,
UsageCard,
} from "@farthershore/farthershore-js/components";
const fs = createFartherShoreClient();
function Application() {
return (
<main className="portal-grid">
<PlansTable />
<UsageCard />
<BillingSummary />
<ApiKeysPanel />
</main>
);
}
export function Portal() {
return (
<FartherShoreRoot client={fs}>
<SignedOut>
<FsSignIn />
</SignedOut>
<SignedIn>
<Application />
</SignedIn>
</FartherShoreRoot>
);
}
FartherShoreRoot provides:
FartherShoreProvider and one cached bootstrap boundary;.fs-app shell and an automatic test-environment badge.The root mounts the selected auth provider and context; it does not implicitly
render a sign-in form. Custom portals must render a managed signed-out surface,
such as <FsSignIn />, and keep private application UI inside <SignedIn>.
Its public customization props are clerk, splash, renderError,
renderCrash, envBadge, skipAppShell, skipAuth, and skipBootGate.
Hosted frontends normally provide only client and children.
For a signed-in customer, application children mount only after the root has
loaded subscription contexts, selected a subscribed organization when one is
available, and confirmed an ACTIVE subscriber whose compiledPlanId is
non-null. Otherwise the root renders the managed workspace picker and plan
onboarding surface. Free onboarding may omit compiledPlanId; Core selects the
current free offer, and the root refetches /me before exposing the app. Paid
onboarding still uses the selected compiled offer. Do not recreate this state
machine from catalog labels or a successful checkout response.
<FartherShoreRoot
client={fs}
splash={<Loading />}
renderError={(error, retry) => (
<button onClick={retry}>Retry: {error.message}</button>
)}
renderCrash={(error, reset) => (
<button onClick={reset}>Reset: {error.message}</button>
)}
envBadge
>
<SignedOut>
<FsSignIn />
</SignedOut>
<SignedIn>
<Application />
</SignedIn>
</FartherShoreRoot>
envBadge defaults on and renders nothing in production. The skip* options
are for hosts deliberately replacing a platform layer; skipping the boot gate
also means useBoot() is unavailable.
Children render after the first successful resolve. Within the root, useBoot()
returns the resolved business, branding, environment, and plans without a null
state:
import { useBoot } from "@farthershore/farthershore-js/react";
function Header() {
const boot = useBoot();
return <h1>{boot.branding.displayName}</h1>;
}
During a background refresh the root keeps the last good bootstrap value instead of unmounting the application.
Under the root, these components work with zero required data props:
| Component | Subscriber surface |
|---|---|
PlansTable | plan catalog and subscription action |
UsageCard | current metered usage |
BillingSummary | subscription and billing state |
ApiKeysPanel | create, rotate, and revoke subscriber keys |
BillPreviewCard | the subscriber's bill preview (transparent or opaque) |
ResourceLimitUsageCard / ResourcesPanel | counted resource usage |
TeamPanel / FsAccessControl | team and managed RBAC |
TrialBanner / CancelSubscription | subscription lifecycle |
BusinessDocs | published business docs |
Each component accepts className and exports a typed props interface. Many
offer optional slots or render callbacks, but the default data source remains
the current SDK client.
BusinessApiReference is a separate rendering primitive and requires a
document prop containing your OpenAPI document. It does not fetch a reference
document automatically. See the API reference example
for the typed input and rendering call.
UsageCard shows recent usage, not a settled invoice. Its snapshot is a bounded
sample marked exact: false. Use BillPreviewCard for platform-rated money and
preserve its transparent/opaque disclosure behavior. Do not sum sampled events
or multiply a displayed allowance label into an amount owed.
The components in the managed vocabulary resolve their own render permission
through the component-policy resolver before showing anything — UsageCard
requires usage:read, ApiKeysPanel requires apikey:read, TeamPanel
requires team:read, and so on. A member whose role lacks the permission
sees an explicit "you don't have access" panel by default; the
security-sensitive components (AuditLog, ApiKeysPanel, TeamPanel) hide
entirely instead. Subscriber organizations can re-gate or change the deny
rendering per component from the access-control panel. Re-gating is
additive for two families: the security-sensitive set keeps its managed
permission as a confidentiality floor, and data-floor components
(FsUsageLimits) keep the permission their data fetch is server-gated on —
in both cases the selected permission is required in addition to the
managed one, never instead of it (the access panel labels these rows).
A few components deliberately do NOT self-gate: presentational surfaces
(TrialBanner, UpgradePrompt, docs/reference views) and inline summaries
designed to compose inside an already-gated page (BillPreviewCard,
ResourceLimitUsageCard / ResourcesPanel, CancelSubscription — its
mutation still gates per-control on subscription:cancel). Mount those
inside a gated route or wrap them yourself. Your own components can join the
managed system — see Permission gates and
Custom components.
Catch typed LimitExceededError values from route calls. You can render a
specific LimitNotice or UpgradePrompt, or mount one global
FsLimitBoundary and report caught errors. Initial prepaid plan purchase is
already handled by PlansTable and FsOnboardingPlanRail through
fs.plans.subscribe() / fs.plans.startOnboarding(). For a later refill there
is no managed component or typed fs.billing method yet: render a
subscriber-owned refill control
that uses the public fs.core()
top-up endpoint.
Do not substitute a builder CLI/MCP call; the subscriber session owns the
purchase.
import {
FsLimitBoundary,
useLimitHandler,
} from "@farthershore/farthershore-js/components";
import { LimitExceededError } from "@farthershore/farthershore-js";
function CreateJob() {
const limits = useLimitHandler();
async function create() {
try {
await fs.route.post("/v1/jobs", {});
} catch (error) {
if (error instanceof LimitExceededError) limits.report(error);
else throw error;
}
}
return <button onClick={() => void create()}>Create job</button>;
}
<FsLimitBoundary>
<CreateJob />
</FsLimitBoundary>;
The boundary changes presentation only. The gateway made the actual limit decision.
Create frontend routes, sidebar entries, layout, and CSS in frontend/.
Business route surfaces constrain which credential surfaces may call an API
operation; they do not generate browser pages or navigation.
Use the stable .fs-* class names as hooks or wrap components with your design
system. Do not expect a package CSS import: the managed repository's stylesheet
is ordinary editable application code.
For a thinner integration, mount FartherShoreProvider from /react directly
and use hooks without the root's boot/auth/shell gates.