Auth & sessions
Sign-in, sessions, and auth-gated routing — managed by the SDK.
Sign-in, sessions, and auth-gated routing — managed by the SDK.
The SDK owns the entire sign-in story so your app never writes auth glue. <FartherShoreRoot> mounts the managed auth layer with the environment's strategy, picked automatically from bootstrap():
clerk — production. The SDK dynamically loads @clerk/clerk-react (an optional peer), mounts it in satellite mode (one shared Clerk instance serves every product host; sign-in happens on the primary domain), and installs Clerk's getToken as the client's per-request token source. The cross-domain handshake is managed for you.test-personas — preview/test environments. The SDK manages its own session: exchange an fsk_test_… persona key for a session, persisted and restored automatically.You read one surface — useFsAuth() — regardless of strategy. Sign-in, the session token, and mid-session 401 recovery are all handled.
Auth is automatic under <FartherShoreRoot>. You only reach for the hooks/components below to render your own sign-in affordances or to gate routes — never to wire the session itself.
useFsAuth() — the one surfaceimport { useFsAuth } from "@farthershore/farthershore-js/components";
function AccountBar() {
const auth = useFsAuth();
if (!auth.loaded) return <Skeleton />; // auth still initializing
return auth.signedIn
? <button onClick={() => void auth.signOut()}>Sign out</button>
: <button onClick={() => auth.signIn()}>Sign in</button>;
}
useFsAuth() returns:
| Field | Meaning |
|---|---|
strategy | "clerk" or "test-personas". |
loaded | false while auth initializes (Clerk JS loading / first session read). |
signedIn | Whether a user is signed in. |
user | The signed-in user normalized to FsAuthUser (Clerk or persona), or null. |
signIn() | Clerk → redirect to the primary hosted sign-in (returning here); persona → scrolls to the in-app form. |
signOut() | Sign out (any strategy). |
pendingSsoRedirect | true while navigating to the primary domain for the silent SSO handshake — keep a splash up. |
refresh() | Re-read the session (persona only; Clerk pushes its own state). |
It throws if used outside <FartherShoreRoot>. Use useOptionalFsAuth() (returns null outside the layer) when a component must degrade instead of throw.
useSession() (from /react) is the lower-level read — { data, loading, error, refresh, signIn, signOut } over the raw fs.auth resource. Prefer useFsAuth() in UI; reach for useSession() when you need the persona signIn({ apiKey }) mutation directly.
Clerk-style declarative wrappers — they read useFsAuth() and render conditionally, never flashing during init:
import {
SignedIn,
SignedOut,
AuthLoading,
FsSignInButton,
FsSignOutButton,
FsUserButton,
} from "@farthershore/farthershore-js/components";
<AuthLoading><Skeleton /></AuthLoading>
<SignedOut><FsSignInButton /></SignedOut>
<SignedIn>
<FsUserButton /> {/* Clerk avatar menu; null in persona mode */}
<FsSignOutButton />
</SignedIn>
FsSignInButton — Clerk → redirects to the primary-domain hosted sign-in; persona → scrolls to the sign-in form.FsUserButton — Clerk's avatar menu (manage-account + sign-out); renders null in persona mode (persona chrome is host-specific — render your own pill).On a test-personas environment there's no hosted sign-in page. You exchange a persona access key (fsk_test_…) for a session. The SDK ships <FsSignIn> for this, or call the session mutation yourself:
import { useSession } from "@farthershore/farthershore-js/react";
function PersonaSignIn() {
const session = useSession();
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const key = new FormData(e.currentTarget).get("apiKey") as string;
await session.signIn({ apiKey: key.trim() }); // exchanges key → session
}}
>
<input name="apiKey" placeholder="fsk_test_…" />
<button type="submit">Sign in</button>
</form>
);
}
You can also drive it imperatively from the raw client: await fs.auth.signIn({ apiKey: "fsk_test_…" }). Mint a fresh fsk_test_ persona key with the CLI — the product is positional, --env is required, and the key is printed once:
farthershore persona bootstrap croncloud --env preview --plan starter
See API keys and test API keys.
Gating "signed-out → bounce to sign-in, signed-in → render" is the most-copied glue, so the SDK lifts it out. The decision is pure and router-agnostic — the SDK never imports a router; your host performs the navigation.
<RequireAuth>Reveal children only when signed in. While auth loads it renders fallback (an aria-busy placeholder by default — no flash of protected content). On a signed-out user it renders fallback and, if you pass onRedirect, calls it with the redirect target so your router navigates.
import { RequireAuth } from "@farthershore/farthershore-js/components";
import { useNavigate } from "react-router-dom";
function CronJobsPage() {
const navigate = useNavigate();
return (
<RequireAuth
fallback={<Splash />}
redirectTo="/" // where a signed-out user goes (default "/")
onRedirect={(to) => navigate(to)}
>
<CronJobsDashboard />
</RequireAuth>
);
}
useAuthGuard() and the pure decisionFor custom route logic, read the decision directly. useAuthGuard reads the managed auth state and returns { status, redirectTo } where status is "loading" | "allowed" | "redirecting". It does not navigate (it optionally stashes the return-to deep link in sessionStorage so you can restore it after sign-in).
import { useAuthGuard } from "@farthershore/farthershore-js/components";
const { status, redirectTo } = useAuthGuard({
requireAuth: true,
redirectTo: "/",
returnToKey: "fs-return-to", // stash location before redirect; null to disable
});
planAuthGuard(input) is the same logic with no React — handy for unit tests and SSR. CronCloud's @Frontend manifest marks the /cron page requiresAuth: true; <RequireAuth> / useAuthGuard is how you honor that in a custom build.
Both strategies recover from a lapsed session automatically, with no host step:
401 on an authed call means the session lapsed; the SDK bounces to the hosted sign-in (returning to the current page). A burst of concurrent 401s triggers a single redirect.401 and proactively at the token's expiresAt, flipping the surface to signed-out cleanly.On the server there's no ambient session. Resolve a per-request bearer via getToken on createServerClient — each request authenticates as a different user.
<FartherShoreRoot>.