React Quickstart ⚛️
Add a complete login experience to a React single-page app: sign in with any connection you’ve enabled (Google, GitHub, email/password, passwordless), read the session with hooks, call your API with the access token, and sign out. The SDK drives the Authorization Code flow with PKCE and refreshes tokens automatically — no protocol code in your app.
You’ll use two packages:
@faable/auth-js— the auth client (PKCE, session storage, auto-refresh, multi-tab sync).@faable/auth-helpers-react— React provider and hooks on top of it.
✅ Prerequisites
In the Faable Dashboard , create a Client for your SPA and configure:
- Allowed Callback URLs:
http://localhost:5173/callback(add your production URL later). - Allowed Logout URLs:
http://localhost:5173. - Allowed Web Origins:
http://localhost:5173.
Note your auth domain (your-domain.auth.faable.link) and Client ID. SPAs are public clients — no client secret is involved.
🛠️ Step 1: Create the App and Install
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @faable/auth-js @faable/auth-helpers-reactStep 2: Create the Auth Client
// src/auth.ts
import { createClient } from "@faable/auth-js";
export const auth = createClient({
domain: "your-domain.auth.faable.link",
clientId: "YOUR_CLIENT_ID",
redirectUri: window.location.origin + "/callback",
});The client initializes itself on creation: it recovers an existing session from storage, or — on the callback URL — exchanges the PKCE ?code= for tokens.
Step 3: Wrap Your App with the Session Provider
// src/main.tsx
import { createRoot } from "react-dom/client";
import { SessionContextProvider } from "@faable/auth-helpers-react";
import { auth } from "./auth";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<SessionContextProvider faableauthClient={auth}>
<App />
</SessionContextProvider>
);The provider waits for initialization, exposes the session, and keeps it updated on login, token refresh, and logout — across browser tabs.
Step 4: Login, User, and Logout
// src/App.tsx
import { useSessionContext, useUser } from "@faable/auth-helpers-react";
import { auth } from "./auth";
import Callback from "./Callback";
export default function App() {
const { isLoading, session, error } = useSessionContext();
const user = useUser();
// The /callback route completes the login (Step 5)
if (window.location.pathname === "/callback") return <Callback />;
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Auth error: {error.message}</p>;
if (!session) {
return (
<button onClick={() => auth.signInWithOauthConnection({})}>
Sign in
</button>
);
}
return (
<div>
<p>Hello {user?.email}</p>
<button onClick={() => auth.signOut({ returnTo: window.location.origin })}>
Sign out
</button>
</div>
);
}signInWithOauthConnection({})sends the user to your tenant’s Universal Login with every connection you’ve enabled. Target one directly with{ connection_id: "connection_..." }.signOut()clears the local session and the SSO cookie on the auth server. ThereturnToURL must be in Allowed Logout URLs.- Both methods redirect the browser on success — the promise intentionally never resolves, so don’t put “re-enable button” code after the
await.
Step 5: The Callback Route
// src/Callback.tsx
import { useEffect, useState } from "react";
import { auth } from "./auth";
export default function Callback() {
const [message, setMessage] = useState("Signing you in…");
useEffect(() => {
auth.handleRedirectCallback().then(({ error, returnTo }) => {
if (error) setMessage(error.message);
else window.location.replace(returnTo ?? "/");
});
}, []);
return <p>{message}</p>;
}handleRedirectCallback() awaits the code-for-tokens exchange (it’s idempotent — the client already started it) and hands you returnTo if you passed one to signInWithOauthConnection({ returnTo }), so deep links survive the login round-trip.
Step 6: Call Your API
The access token lives on the session. Read it fresh before each call — getSession() auto-refreshes an expired session:
import { auth } from "./auth";
export async function apiFetch(path: string) {
const { data, error } = await auth.getSession();
if (error || !data.session) throw new Error("Not signed in");
return fetch(`https://api.myapp.com${path}`, {
headers: { authorization: `Bearer ${data.session.access_token}` },
});
}If your backend validates the token’s audience, pass your API identifier when creating the client (createClient({ ..., audience: "https://api.myapp.com" })) — and see Validate Access Tokens for the Express middleware on the other side.
❓ FAQ
How do I get the access token?
From the session: const { data } = await auth.getSession(), then data.session?.access_token. There is no separate getAccessToken() method — getSession() already refreshes expired tokens before returning.
Do I need to handle token refresh?
No. The SDK refreshes sessions automatically in the background using the Refresh Token flow, and syncs the result across tabs.
How do users pick a login method?
By default they choose on the Universal Login screen among the connections enabled for your client. To skip the screen and go straight to one provider, pass connection_id to signInWithOauthConnection.
Why does my logout return a 400?
The returnTo URL must be registered in the client’s Allowed Logout URLs in the dashboard — same rule as callback URLs for login.
Can errors throw somewhere unexpected?
No — every SDK method resolves { data, error } and never throws for expected failures. Only createClient itself throws, when domain or clientId is missing.
🔗 Related
- Next.js Quickstart — the same login for Next.js apps.
- React Native Quickstart — Expo / mobile.
- Authorization Code Flow — what the SDK does under the hood.
- Validate Access Tokens — verify these tokens in your backend.
- Connections — enable Google, GitHub, passwordless, and more.
Last updated on