Frontend SDK
Wire a portal or custom UI to the platform with @farthershore/farthershore-js.
Wire a portal or custom UI to the platform with @farthershore/farthershore-js.
@farthershore/farthershore-js is the browser integration layer between your frontend and the platform. Your UI expresses intent — "list my keys", "show usage", "call the cron-jobs feature" — and the SDK decides where each request goes (Core for platform state, the Gateway for your product's features), how it's authenticated, and what host/env scoping headers to attach. Frontend code never sees a Core URL, a Gateway URL, your backend origin, or an auth endpoint.
It ships in lockstep with @farthershore/product and @farthershore/backend: pin all three to the same version.
pnpm add @farthershore/farthershore-js # React is an optional peer (for /react)
Every product gets a generic managed portal for free — the platform serves it from R2 with no code from you. You only touch the frontend when you want to customize it:
frontend/ tree (the SDK-wired scaffold), push, and the platform rebuilds it into a custom release and serves it instead. You author with the SDK below.The scaffold is already wired: one client under one <FartherShoreRoot> rendering the zero-prop data components.
createFartherShoreClient builds the client. In the browser, coreUrl is usually the only field — portalHost defaults to window.location.host, and bootstrap() discovers the product, environment, and plans for that host.
import { createFartherShoreClient } from "@farthershore/farthershore-js";
const fs = createFartherShoreClient({
coreUrl: "https://core.farthershore.com",
});
// Discover the product/env/plans for this host (memoized; safe to re-call).
const { product, plans } = await fs.bootstrap();
// Read platform state through typed resources (→ Core).
const keys = await fs.keys.list(); // ApiKey[]
const usage = await fs.usage.snapshot(); // { summary, events, billingBasis, … }
// Call a product-defined feature (→ Gateway) by name + path — no Gateway URL.
fs.setApiKey("fsk_live_…");
const jobs = await fs.feature("cron-jobs").json("/v1/cron-jobs");
createFartherShoreClient.fromEnv() — the scaffold entryPortals served through the platform edge need no config at all: the edge injects window.__FS_CONFIG__ (the environment's coreUrl plus the Clerk connection) into the served HTML, and the SDK falls back to it. The scaffold uses .fromEnv() so the same code works in prod (injected) and local dev (bundler env override):
// The frontend ships env-agnostic; in prod the edge injects the config. The
// override is a dev/self-host escape hatch — undefined in prod, so it's ignored.
const fs = createFartherShoreClient.fromEnv({
coreUrl: import.meta.env.VITE_FS_CORE_URL, // dev/self-host only
});
Explicit config always wins. Everywhere there's no injected channel (local dev, SSR, self-hosting), coreUrl is required, or the client throws an actionable coreUrl is required error.
createFartherShoreClient({
coreUrl, // platform Core base URL — optional on platform-served portals
portalHost, // defaults to window.location.host
productId, // optional — bootstrap() discovers it
environmentId, // ephemeral/preview env scoping
organizationId, // org-owned subscriptions
gatewayUrl, // override (else derived from the product's runtimeHostname)
apiKey, // consumer key for Gateway feature calls
getToken, // () => session bearer (Clerk) | null
fetch, // injectable (tests / SSR)
});
The hooks live in the /react subpath (react is an optional peer). Wrap your tree in FartherShoreProvider once at the root, then every read is a hook returning { data, loading, error, refresh } plus its mutations.
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
FartherShoreProvider,
useApiKeys,
useUsage,
} from "@farthershore/farthershore-js/react";
// Build the client at module scope (a stable reference — never per render).
const fs = createFartherShoreClient.fromEnv({
coreUrl: import.meta.env.VITE_FS_CORE_URL,
});
function App() {
return (
<FartherShoreProvider client={fs}>
<Portal />
</FartherShoreProvider>
);
}
function Keys() {
const { data, loading, create, revoke } = useApiKeys();
if (loading) return <p>Loading…</p>;
return <ul>{data?.map((k) => <li key={k.id}>{k.label}</li>)}</ul>;
}
FartherShoreProvider takes either a pre-built client (preferred — you own its lifetime) or a config it builds internally (pass a stable object so the client isn't recreated each render).
The hooks: useBootstrap, useProduct, useSession, useApiKeys, useUsage, useBilling, usePlans, useCreditBalance, useEntitlements, useFeatureGate, useCapability, useFeature(name), plus useFartherShore() for the raw client. They're thin wrappers — no duplicated logic.
For a batteries-included root that also handles bootstrap, theming-from-branding, and managed auth, prefer <FartherShoreRoot> over a bare <FartherShoreProvider>. It wraps the provider and adds the gate.
| Namespace | Routes to | Auth |
|---|---|---|
fs.bootstrap(), fs.product | Core (public resolve) | none (public) |
fs.keys, fs.usage, fs.billing, fs.plans, fs.auth | Core | consumer session token (Authorization: Bearer) |
fs.feature(name) / fs.invoke(path) | Gateway | consumer API key (fsk_…) |
getToken; persona/preview envs call fs.auth.signIn({ apiKey: "fsk_test_…" }). See Auth & sessions.fsk_ API key: fs.setApiKey(key). The gateway host is discovered at bootstrap.On a server there's no window/location and no ambient session. Use createServerClient — a documented alias that makes that contract explicit: pass portalHost and fetch, and resolve the per-request session via getToken.
import { createServerClient } from "@farthershore/farthershore-js";
const fs = createServerClient({
coreUrl: process.env.FS_CORE_URL!,
portalHost: "cron.farthershore.com", // no window.location on the server
getToken: () => sessionTokenForThisRequest, // per-request bearer
fetch, // Node 18+ global, or injected
});
const sub = await fs.billing.subscription();
Reads and mutations throw typed errors you can branch on — FartherShoreApiError (with a .code from FS_DENY_CODES), LimitExceededError, FeatureNotGrantedError, FartherShoreRateLimitedError, and more. Pair a LimitExceededError with <UpgradePrompt>, a FeatureNotGrantedError with a <FeatureGate> upsell. See response codes for the wire vocabulary.
<FartherShoreRoot> and the zero-prop cards.