Skip to content

Commit fcfc289

Browse files
committed
docs(auth): React SPA quickstart (Vite)
Grounded in @faable/auth-js and @faable/auth-helpers-react sources: createClient config, SessionContextProvider + useSessionContext/useUser hooks, handleRedirectCallback with returnTo round-trip, session-based access token (no getAccessToken method), never-resolving redirect promises, signOut logout-URL requirement, { data, error } contract. Quickstarts moved to the top of the sidebar with explicit ordering; linked from get-started.
1 parent 149b2a0 commit fcfc289

4 files changed

Lines changed: 193 additions & 0 deletions

File tree

β€Žcontent/auth/_meta.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default {
22
"get-started": "Get Started",
3+
quickstart: "Quickstarts",
34
"what-is-a-multi-tenant-identity-server":
45
"What is a multi-tenant identity server?",
56
compare: "Faable Auth vs Auth0, Clerk & Keycloak",

β€Žcontent/auth/get-started.mdβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Pick the path that matches your stack.
3333

3434
The fastest way to add login to an existing app.
3535

36+
- **[React Quickstart](quickstart/react.md)** β€” Vite SPA + session hooks from `@faable/auth-helpers-react`.
3637
- **[Next.js Quickstart](quickstart/nextjs.md)** β€” App Router + client SDK with PKCE.
3738
- **[React Native Quickstart](quickstart/react-native.md)** β€” Expo + Faable Auth helpers.
3839

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
react: "React (SPA)",
3+
nextjs: "Next.js",
4+
"react-native": "React Native",
5+
};
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
---
2+
schema: faq
3+
title: React Quickstart β€” Add Login to a Vite SPA
4+
description: Add authentication to a React single-page app with Faable Auth in minutes β€” @faable/auth-js with PKCE, session hooks from @faable/auth-helpers-react, login, logout, and API calls.
5+
---
6+
7+
# React Quickstart βš›οΈ
8+
9+
Add a complete login experience to a React single-page app: sign in with any [connection](../connections.md) 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](../oauth-flows/authorization-code.md) and refreshes tokens automatically β€” no protocol code in your app.
10+
11+
You'll use two packages:
12+
13+
- **[`@faable/auth-js`](https://www.npmjs.com/package/@faable/auth-js)** β€” the auth client (PKCE, session storage, auto-refresh, multi-tab sync).
14+
- **[`@faable/auth-helpers-react`](https://www.npmjs.com/package/@faable/auth-helpers-react)** β€” React provider and hooks on top of it.
15+
16+
---
17+
18+
## βœ… Prerequisites
19+
20+
In the [Faable Dashboard](https://dashboard.faable.com), create a **Client** for your SPA and configure:
21+
22+
- **Allowed Callback URLs:** `http://localhost:5173/callback` (add your production URL later).
23+
- **Allowed Logout URLs:** `http://localhost:5173`.
24+
- **Allowed Web Origins:** `http://localhost:5173`.
25+
26+
Note your **auth domain** (`your-domain.auth.faable.link`) and **Client ID**. SPAs are public clients β€” no client secret is involved.
27+
28+
---
29+
30+
## πŸ› οΈ Step 1: Create the App and Install
31+
32+
```bash
33+
npm create vite@latest my-app -- --template react-ts
34+
cd my-app
35+
npm install @faable/auth-js @faable/auth-helpers-react
36+
```
37+
38+
## Step 2: Create the Auth Client
39+
40+
```ts
41+
// src/auth.ts
42+
import { createClient } from "@faable/auth-js";
43+
44+
export const auth = createClient({
45+
domain: "your-domain.auth.faable.link",
46+
clientId: "YOUR_CLIENT_ID",
47+
redirectUri: window.location.origin + "/callback",
48+
});
49+
```
50+
51+
The client initializes itself on creation: it recovers an existing session from storage, or β€” on the callback URL β€” exchanges the PKCE `?code=` for tokens.
52+
53+
## Step 3: Wrap Your App with the Session Provider
54+
55+
```tsx
56+
// src/main.tsx
57+
import { createRoot } from "react-dom/client";
58+
import { SessionContextProvider } from "@faable/auth-helpers-react";
59+
import { auth } from "./auth";
60+
import App from "./App";
61+
62+
createRoot(document.getElementById("root")!).render(
63+
<SessionContextProvider faableauthClient={auth}>
64+
<App />
65+
</SessionContextProvider>
66+
);
67+
```
68+
69+
The provider waits for initialization, exposes the session, and keeps it updated on login, token refresh, and logout β€” across browser tabs.
70+
71+
## Step 4: Login, User, and Logout
72+
73+
```tsx
74+
// src/App.tsx
75+
import { useSessionContext, useUser } from "@faable/auth-helpers-react";
76+
import { auth } from "./auth";
77+
import Callback from "./Callback";
78+
79+
export default function App() {
80+
const { isLoading, session, error } = useSessionContext();
81+
const user = useUser();
82+
83+
// The /callback route completes the login (Step 5)
84+
if (window.location.pathname === "/callback") return <Callback />;
85+
86+
if (isLoading) return <p>Loading…</p>;
87+
if (error) return <p>Auth error: {error.message}</p>;
88+
89+
if (!session) {
90+
return (
91+
<button onClick={() => auth.signInWithOauthConnection({})}>
92+
Sign in
93+
</button>
94+
);
95+
}
96+
97+
return (
98+
<div>
99+
<p>Hello {user?.email}</p>
100+
<button onClick={() => auth.signOut({ returnTo: window.location.origin })}>
101+
Sign out
102+
</button>
103+
</div>
104+
);
105+
}
106+
```
107+
108+
- `signInWithOauthConnection({})` sends the user to your tenant's Universal Login with every connection you've enabled. Target one directly with `{ connection_id: "connection_..." }`.
109+
- `signOut()` clears the local session **and** the SSO cookie on the auth server. The `returnTo` URL must be in **Allowed Logout URLs**.
110+
- Both methods redirect the browser on success β€” the promise intentionally never resolves, so don't put "re-enable button" code after the `await`.
111+
112+
## Step 5: The Callback Route
113+
114+
```tsx
115+
// src/Callback.tsx
116+
import { useEffect, useState } from "react";
117+
import { auth } from "./auth";
118+
119+
export default function Callback() {
120+
const [message, setMessage] = useState("Signing you in…");
121+
122+
useEffect(() => {
123+
auth.handleRedirectCallback().then(({ error, returnTo }) => {
124+
if (error) setMessage(error.message);
125+
else window.location.replace(returnTo ?? "/");
126+
});
127+
}, []);
128+
129+
return <p>{message}</p>;
130+
}
131+
```
132+
133+
`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.
134+
135+
## Step 6: Call Your API
136+
137+
The access token lives on the session. Read it fresh before each call β€” `getSession()` auto-refreshes an expired session:
138+
139+
```ts
140+
import { auth } from "./auth";
141+
142+
export async function apiFetch(path: string) {
143+
const { data, error } = await auth.getSession();
144+
if (error || !data.session) throw new Error("Not signed in");
145+
146+
return fetch(`https://api.myapp.com${path}`, {
147+
headers: { authorization: `Bearer ${data.session.access_token}` },
148+
});
149+
}
150+
```
151+
152+
If your backend validates the token's `audience`, pass your [API](../apis.md) identifier when creating the client (`createClient({ ..., audience: "https://api.myapp.com" })`) β€” and see [Validate Access Tokens](../validate-access-tokens.md) for the Express middleware on the other side.
153+
154+
---
155+
156+
## ❓ FAQ
157+
158+
### How do I get the access token?
159+
160+
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.
161+
162+
### Do I need to handle token refresh?
163+
164+
No. The SDK refreshes sessions automatically in the background using the [Refresh Token flow](../oauth-flows/refresh-token.md), and syncs the result across tabs.
165+
166+
### How do users pick a login method?
167+
168+
By default they choose on the Universal Login screen among the [connections](../connections.md) enabled for your client. To skip the screen and go straight to one provider, pass `connection_id` to `signInWithOauthConnection`.
169+
170+
### Why does my logout return a 400?
171+
172+
The `returnTo` URL must be registered in the client's **Allowed Logout URLs** in the dashboard β€” same rule as callback URLs for login.
173+
174+
### Can errors throw somewhere unexpected?
175+
176+
No β€” every SDK method resolves `{ data, error }` and never throws for expected failures. Only `createClient` itself throws, when `domain` or `clientId` is missing.
177+
178+
---
179+
180+
## πŸ”— Related
181+
182+
- **[Next.js Quickstart](nextjs.md)** β€” the same login for Next.js apps.
183+
- **[React Native Quickstart](react-native.md)** β€” Expo / mobile.
184+
- **[Authorization Code Flow](../oauth-flows/authorization-code.md)** β€” what the SDK does under the hood.
185+
- **[Validate Access Tokens](../validate-access-tokens.md)** β€” verify these tokens in your backend.
186+
- **[Connections](../connections.md)** β€” enable Google, GitHub, passwordless, and more.

0 commit comments

Comments
Β (0)