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 { SessionContextProvider } from '@faable/auth-helpers-react' import { createRoot } from 'react-dom/client' import App from './App' import { auth } from './auth' 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 Callback from './Callback' import { auth } from './auth' 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