Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
useFsAuth() — the one surfaceControl componentsPersona sign-in (preview / test)Auth-gated routingMid-session expiry — handled for youServer-side sessionsNext
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Build the frontend/Auth & sessions

Auth & sessions

Sign-in, sessions, and auth-gated routing — managed by the SDK.

PreviousRoot & data componentsNextEntitlement gates

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 surface

import { 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:

FieldMeaning
strategy"clerk" or "test-personas".
loadedfalse while auth initializes (Clerk JS loading / first session read).
signedInWhether a user is signed in.
userThe 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).
pendingSsoRedirecttrue 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.

Control components

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).

Persona sign-in (preview / test)

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.

Auth-gated routing

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 decision

For 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.

Mid-session expiry — handled for you

Both strategies recover from a lapsed session automatically, with no host step:

  • Clerk: a 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.
  • Persona: the JWT is a fixed 24h with no refresh, so the SDK signs out on a 401 and proactively at the token's expiresAt, flipping the surface to signed-out cleanly.

Server-side sessions

On the server there's no ambient session. Resolve a per-request bearer via getToken on createServerClient — each request authenticates as a different user.

Next

  • Entitlement gates — gate UI by plan capability and feature.
  • Root & data components — the auth layer is mounted by <FartherShoreRoot>.