Skip to Content
🔐 Faable AuthQuickstartsReact (SPA)

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:


✅ 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-react

Step 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. The returnTo URL 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.


Last updated on

Last updated on