@farthershore/farthershore-js
v0.15.0
Published
Farther Shore Frontend SDK — the browser integration layer between static frontends and the Farther Shore platform (Core + Gateway).
Maintainers
Readme
@farthershore/farthershore-js
The Farther Shore Frontend SDK — the browser + React integration layer for your product's subscriber portal.
Status:
0.15.0. Pre-1.0 (0.x): a minor bump (e.g.0.9.x→0.10.0) may include breaking changes, so a caret range is unsafe — pin this package exactly (0.9.0) or use a patch-only tilde (~0.9.0). Changes are recorded inCHANGELOG.md.
pnpm add @farthershore/farthershore-js # React is an optional peer (for /react)What this is
When you build a software business on Farther Shore, your subscribers get a portal where they sign in, pick a plan, manage API keys, watch their usage, and handle billing — and where you can surface your own product features. This SDK is how a frontend talks to that portal.
Your frontend code never deals with Core URLs, Gateway URLs, auth endpoints, or
routing rules. It expresses intent — "list my API keys", "subscribe to this
plan", "call my weather feature" — and the SDK decides where each request
goes (Core for platform concerns like auth, plans, usage, and billing; the
Gateway for your product's own features), how it's authenticated, and which
host/environment scoping headers to attach.
This guide walks through the five on-ramps for integrating the SDK into a real app: the zero-config happy path, bringing your own auth, bringing your own design system, bringing your own data layer, and writing org-scoped custom hooks.
1. Zero-config happy path
On a portal served by the Farther Shore edge, the client needs no
arguments at all. It reads window.__FS_CONFIG__ (injected by the edge)
lazily — the first time a request actually needs it — and discovers the
businessId from the portal host.
import { createFartherShoreClient } from "@farthershore/farthershore-js";
const fs = createFartherShoreClient();Wrap your tree in the provider:
import { FartherShoreProvider } from "@farthershore/farthershore-js/react";
function App() {
return (
<FartherShoreProvider client={fs}>
<Portal />
</FartherShoreProvider>
);
}Then read platform state through the hooks. Every read hook returns the same
ecosystem shape — { data, error, isLoading, isError, isSuccess, refetch,
queryKey } — plus its mutations:
import { useApiKeys } from "@farthershore/farthershore-js/react";
function ApiKeys() {
const { data, isLoading, isError, error, refetch } = useApiKeys();
if (isLoading) return <p>Loading…</p>;
if (isError) return <p>Failed to load keys: {error?.message}</p>;
return (
<ul>
{data?.map((k) => (
<li key={k.id}>{k.label}</li>
))}
</ul>
);
}Want a whole portal with almost no code? <FartherShoreRoot> is one wrapper —
provider + bootstrap gate + theme + managed auth — and the managed component
kit is self-fed under it:
import {
FartherShoreRoot,
PlansTable,
UsageCard,
BillingSummary,
ApiKeysPanel,
} from "@farthershore/farthershore-js/components";
// The SDK is HEADLESS — it ships zero CSS. Provide the component styles
// yourself. A platform-provisioned repo already includes them as plain,
// editable CSS:
import "./styles/components.css";
<FartherShoreRoot client={fs}>
<PlansTable />
<UsageCard />
<BillingSummary />
<ApiKeysPanel />
</FartherShoreRoot>;Escape hatch (dev/test only): everywhere there's no injected
window.__FS_CONFIG__ — local dev, self-hosting, tests — pass config
explicitly:
const fs = createFartherShoreClient({
coreUrl: "http://localhost:4000", // dev/self-host only; injected on-platform
portalHost: "croncloud.example.com", // defaults to window.location.host
businessId: "biz_123", // usually omitted — bootstrap() discovers it
getToken: () => myAuthProvider.getToken(), // see "BYO auth" below
});Explicit config always wins over the injected channel. A request attempted
with no resolvable coreUrl throws an actionable FartherShoreConfigError.
2. BYO auth
Feed the client your own session token instead of the managed Clerk/persona layer, in one of two ways:
At construction, via
getTokenin the config:const fs = createFartherShoreClient({ getToken: () => myAuthProvider.getToken(), // () => string | null | Promise<...> });After construction, via the client method (useful when the token provider isn't ready yet at client-build time):
fs.setTokenProvider(() => myAuthProvider.getToken());
If you're using <FartherShoreRoot> but want to bring your own auth provider
entirely (skip the managed Clerk-satellite/persona layer), pass skipAuth:
<FartherShoreRoot client={fs} skipAuth>
<MyOwnAuthProvider>
<Portal />
</MyOwnAuthProvider>
</FartherShoreRoot>3. BYO design system
This package is headless: every component renders semantic markup with
stable .fs-* class names and ships no CSS at all. You own 100% of the
styling — write it against the .fs-* class hooks (and the --fs-* CSS
variable tokens the components read for theming). A platform-provisioned repo
comes with a full default stylesheet as plain, editable CSS
(src/styles/components.css); import it, edit it, or replace it wholesale.
The managed components work standalone under a bare <FartherShoreProvider>
— you don't need <FartherShoreRoot> to use them:
import { FartherShoreProvider } from "@farthershore/farthershore-js/react";
import {
PlansTable,
ApiKeysPanel,
UsageCard,
BillingSummary,
CreditBalance,
TeamPanel,
AuditLogTable,
RateLimitDisplay,
BillEstimator,
CapabilityUsageCard,
UpgradePrompt,
TopUpPrompt,
DocsLegal,
ResourcesPanel,
FeaturePanel,
} from "@farthershore/farthershore-js/components";
import "./styles/components.css"; // your own styles for the .fs-* class hooks
<FartherShoreProvider client={fs}>
<PlansTable />
<ApiKeysPanel />
</FartherShoreProvider>;Recompose without forking. PlansTable takes renderRow for a fully custom
card, with context for the featured/pending/subscribe state:
<PlansTable
renderRow={(plan, { featured, pending, onSubscribe, index }) => (
<MyPlanCard
key={plan.id}
plan={plan}
highlighted={featured}
busy={pending}
onClick={onSubscribe}
rank={index}
/>
)}
/>UsageCard takes renderRow per metered dimension:
<UsageCard
renderRow={(row, index) => (
<MyUsageRow key={row.key} row={row} index={index} />
)}
/>BillingSummary takes named slots (e.g. to replace the actions area):
<BillingSummary slots={{ actions: <MyBillingActions /> }} />Every component also accepts a plain className on its root for CSS-based
restyling.
To bring your own theming under <FartherShoreRoot> instead of the managed
theme root, pass skipTheme:
<FartherShoreRoot client={fs} skipTheme>
<MyThemeProvider>
<Portal />
</MyThemeProvider>
</FartherShoreRoot>4. BYO data layer
Already run TanStack Query or SWR? The /react subpath ships a structural
adapter — no runtime dependency on either library, so it costs nothing unless
you import it.
TanStack Query:
import { useQuery } from "@tanstack/react-query";
import { fsQueryOptions } from "@farthershore/farthershore-js/react";
const q = fsQueryOptions(fs);
const { data } = useQuery(q.keys.list());
const { data: usage } = useQuery(q.usage.snapshot());SWR:
import useSWR from "swr";
import { createFsFetcher } from "@farthershore/farthershore-js/react";
const fetcher = createFsFetcher(fs);
const { data } = useSWR("https://api.example.com/v1/x", fetcher);fsQueryOptions covers the common reads (keys, usage, billing, plans,
me, business). For a product-declared resource beyond those, use the
generic escape hatch — both return a ready-to-use { queryKey, queryFn }
object:
const { data: widgets } = useQuery(q.resource("widgets"));
const { data: widget } = useQuery(q.resourceItem("widgets", widgetId));5. Org-scope custom hooks
Writing your own hook that reads org-scoped data (a product-declared resource,
say)? Use useReadScope() instead of useFartherShore() alone — it composes
the client with the reactive active-org id, so folding it into your query
key/dep array makes an org switch auto-refetch:
import { useReadScope } from "@farthershore/farthershore-js/react";
import { useAsync } from "@farthershore/farthershore-js/react";
function useMyResource() {
const { fs, businessId, organizationId } = useReadScope();
return useAsync(
(signal) => fs.resources("widgets").list({ signal }),
[fs, businessId, organizationId],
["widgets", businessId, organizationId],
);
}fs alone is a stable reference and never reacts to an org switch on its
own — businessId/organizationId are what make the dep array and query key
scope-correct.
Errors
Reads and mutations throw typed errors you can catch and branch on. The root
package keeps the five most-caught classes:
import {
FartherShoreError,
FartherShoreApiError,
LimitExceededError,
FartherShoreRateLimitedError,
FartherShoreConfigError,
} from "@farthershore/farthershore-js";
try {
await fs.keys.list();
} catch (err) {
if (err instanceof LimitExceededError) showUpgrade(err);
else if (err instanceof FartherShoreApiError && err.status === 401) {
// session expired — prompt re-auth
} else throw err;
}The full taxonomy — throttle subclasses (ConcurrencyLimitError,
AdaptiveThrottleError), deny-envelope parsers, makeApiError, secure-fetch
codes, and more — lives at the @farthershore/farthershore-js/errors subpath.
Display helpers
Pure catalog/pricing/usage formatting helpers are grouped under the format
namespace, so every frontend renders money, dates, and quotas identically:
import { format } from "@farthershore/farthershore-js";
format.formatCents(1234); // "$12.34"Testing
@farthershore/farthershore-js/test-utils gives you a fully-typed mock client,
a provider wrapper, and typed-error builders — no network, no server:
import {
createMockFartherShoreClient,
FartherShoreTestProvider,
mockLimitExceeded,
} from "@farthershore/farthershore-js/test-utils";
const client = createMockFartherShoreClient({
keys: { list: async () => [{ id: "key_1", label: "Test key" }] },
});
render(
<FartherShoreTestProvider client={client}>
<ApiKeys />
</FartherShoreTestProvider>,
);mockLimitExceeded, mockRateLimited, and mockApiError build the typed
errors above for asserting your error-handling paths without a real deny from
Core or the Gateway.
Learn more
See the platform docs for the full guide to building, publishing, and operating a
product on Farther Shore. Breaking changes within 0.x are recorded in
CHANGELOG.md — read it before bumping.
