Auth & sessions
Use the environment-selected subscriber auth strategy and gate application routes without owning token plumbing.
<FartherShoreRoot> selects the subscriber auth strategy from the environment's
bootstrap data and mounts the matching provider:
clerkuses the platform-injected Clerk connection and installs the current subscriber session token on SDK requests;test-personasreads Core's server-owned browser session through the SDK;- local mock mode supplies a deterministic signed-in owner without making network requests.
These are subscriber sessions inside the hosted frontend. They are separate from builder CLI login and backend runtime tokens.
<FartherShoreRoot> mounts the provider and auth context, but it does not render
a sign-in surface for signed-out visitors. A custom portal must render a managed
auth component explicitly and keep its application under the signed-in gate.
Mount auth once
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
FartherShoreRoot,
FsSignIn,
SignedIn,
SignedOut,
} from "@farthershore/farthershore-js/components";
const fs = createFartherShoreClient();
export function App() {
return (
<FartherShoreRoot client={fs}>
<SignedOut>
<FsSignIn />
</SignedOut>
<SignedIn>
<Application />
</SignedIn>
</FartherShoreRoot>
);
}
Hosted environments normally need no Clerk prop: the edge injects the public
connection into window.__FS_CONFIG__. A custom non-hosted shell can pass a
public Clerk configuration to the root, but it must never embed a secret key.
Read the normalized auth surface
useFsAuth() presents the same shape for either live strategy:
import { useFsAuth } from "@farthershore/farthershore-js/react";
function AccountButton() {
const auth = useFsAuth();
if (!auth.loaded) return <span aria-busy="true" />;
return auth.signedIn ? (
<button onClick={() => void auth.signOut()}>Sign out</button>
) : null;
}
The important fields are:
| Field | Meaning |
|---|---|
strategy | clerk or test-personas |
loaded | auth initialization has completed |
signedIn | a subscriber session is present |
user | normalized current user or null |
signOut() | revokes the active server session |
roles, permissions, hasPermission() | server-resolved member authorization |
authzLoaded | permission resolution has completed |
useFsAuth() must be under the managed auth provider. Use
useOptionalFsAuth() only for a shared component that intentionally renders
outside it.
The component subpath also exports SignedIn, SignedOut, AuthLoading,
FsSignInButton, FsSignOutButton, and FsUserButton for declarative chrome.
Preview persona browser sessions
Create a persona, then start its browser session from the authenticated CLI:
farthershore persona bootstrap my-business \
--env preview \
--idempotency-key <persisted-persona-bootstrap-attempt-key> \
--plan starter
farthershore persona login my-business <personaId> --env preview
persona login opens the platform-owned /persona-sign-in page on the hosted
portal origin. Its single-use handoff secret is carried only in the URL fragment,
which the page clears before making a same-origin exchange. Core then sets a
server-owned HttpOnly cookie and the page returns to the requested portal route.
The custom frontend bundle never receives the handoff secret, a bearer, or an
access key.
The bootstrap key is returned once for CLI and gateway testing only. Do not paste it into a web form, add it to frontend configuration, or persist it in browser storage. A persona key belongs to its selected preview environment and is not a production credential.
Use the safe identity projection on the SDK session to render custom persona-aware UI:
import { useSession } from "@farthershore/farthershore-js/react";
function PersonaName() {
const session = useSession();
const persona = session.data?.authSession;
if (!persona) return null;
return <span>{persona.displayName ?? persona.userId}</span>;
}
Local live preview as a persona
While building the frontend, run the checkout itself against the real environment, already signed in, with hot reload (CLI 0.33.5+):
farthershore frontend dev --live --business my-business --env preview \
--persona <personaId-or-name> --port 5173 --format json
The CLI resolves the business, environment, and persona, issues a short-lived
local-preview lease that never leaves the CLI process, starts Vite on a private
loopback port, and serves http://localhost:5173 through a CLI-owned proxy:
/_fs/api/* calls carry the lease to Core, /_fs/secure/* (fs.fetch) goes to
the environment's gateway, and everything else reaches Vite with the session
cookie, Authorization, and all trust headers stripped — including HMR
WebSocket upgrades. The browser opens a platform-owned /persona-sign-in page
on localhost whose URL carries no secret; the proxy attaches the one-time
handoff itself and Core sets the same server-owned HttpOnly cookie as the hosted
portal. frontend preview serves the production bundle the same way.
The JSON envelope is emitted only once the preview is signed in and carries
localUrl, signInUrl, and vitePort. The lease renews itself while the
command runs; SIGINT, SIGTERM, or SIGHUP stops Vite and revokes it, and the
cookie stops authenticating immediately. Lifecycle notices (a renewal that keeps
failing, Vite exiting) are written to stderr in every output mode. --mock and
--core-url cannot be combined with --persona. The
persona.local_preview.issue|renew|revoke operations exist only for this
command; never call them by hand.
authSession contains only verified, non-secret session metadata. It is not a
token source. For a custom logout control, call fs.auth.signOut() (or the
signOut() returned by useSession()); it asks Core to revoke the server cookie
before the SDK clears its local read caches. If revocation fails, retain the
authenticated UI and surface the failure rather than pretending the browser is
signed out.
Gate an application route
The SDK is router-agnostic. <RequireAuth> renders only after auth is allowed;
your router performs navigation through onRedirect:
import { RequireAuth } from "@farthershore/farthershore-js/components";
import { useNavigate } from "react-router-dom";
function JobsPage() {
const navigate = useNavigate();
return (
<RequireAuth
fallback={<p>Signing in…</p>}
redirectTo="/"
onRedirect={(target) => navigate(target)}
>
<Jobs />
</RequireAuth>
);
}
For custom routing, useAuthGuard({ requireAuth: true }) returns loading,
allowed, redirecting, or denied plus a redirect target when relevant. It
does not navigate. By default it stores the current path in sessionStorage
under fs-return-to; this is a return-location hint, never a credential. Pass
returnToKey: null to disable that behavior.
RequireAuth and useAuthGuard can also require one permission, but that check
only controls presentation. The gateway remains the security boundary for the
route call, and the backend must still scope application records to the verified
organization.
Session failure behavior
The SDK uses the active provider for every request instead of asking application code to cache credentials. When Core no longer accepts the persona cookie, the managed provider returns the UI to its signed-out flow. Application code should handle the typed 401/permission/limit errors from the requested operation and must never copy a bearer or access key into browser storage.