Next.js
Add Faable Auth to your Next.js application to easily authenticate your users. This guide shows you how to integrate the Faable Auth login flow using the secure Authorization Code Flow with PKCE, meaning the token exchange happens automatically and securely on the client side.
The examples below cover both the App Router (app/, Next.js 13+) and the Pages Router (pages/). Pick the tab that matches your project.
Prerequisites
Before you start, you need to prepare your Faable Auth environment:
-
Faable Auth Domain. In the Faable Dashboard , go to Auth → (your account) → Settings → Auth Configuration → Domain. That value (e.g.
your-tenant.auth.faable.link) is what you’ll pass asdomainbelow.The Custom Domains section on that same Settings page is optional — it lets you serve the hosted login under your own brand (e.g.
auth.yourcompany.com). You do not need to create one to follow this guide; the default*.auth.faable.linkworks out of the box. See Custom Domain if you want to set one up later. -
Client. In the dashboard, create a new Client for your application and note its Client ID. Configure the Allowed Callback URLs to include your callback (e.g.
http://localhost:3000/callbackfor local development).
Installation
Install the required Faable packages in your Next.js project:
npm install @faable/auth-js @faable/auth-helpers-reactInitialize the Faable Auth client
Create a shared client instance you can import from anywhere in your app.
// lib/faable.ts
import { createClient } from "@faable/auth-js";
export const faableauth = createClient({
domain: "your-tenant.auth.faable.link", // from Dashboard → Auth → Settings → Domain
clientId: "<your_client_id>",
});Add the session provider
The SessionContextProvider from @faable/auth-helpers-react exposes the current session to your components. In the App Router it must live inside a "use client" boundary; in the Pages Router it goes in _app.tsx.
App Router
Create a small client component that owns the provider:
// app/providers/ClientSide.tsx
"use client";
import { SessionContextProvider } from "@faable/auth-helpers-react";
import { faableauth } from "../../lib/faable";
export function ClientSide({ children }: { children: React.ReactNode }) {
return (
<SessionContextProvider faableauthClient={faableauth}>
{children}
</SessionContextProvider>
);
}Then wrap your root layout with it. The layout itself stays a server component — only ClientSide is client-side.
// app/layout.tsx
import { ClientSide } from "./providers/ClientSide";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ClientSide>{children}</ClientSide>
</body>
</html>
);
}Add the callback route
After login the user is redirected to the callback URL you registered on your Client. @faable/auth-js reads the tokens from the URL automatically, but your callback route should await that processing so it can:
- redirect only once the token exchange has finished, and
- show an error instead of getting stuck on “Signing you in…” if the exchange fails (e.g. an expired login attempt).
Call handleRedirectCallback() for exactly that. It returns { error, returnTo }: on success send the user onward (to returnTo if you set one when starting the login — see below), and on failure surface error.message.
App Router
// app/callback/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { faableauth } from "../../lib/faable";
export default function CallbackPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
faableauth.handleRedirectCallback().then(({ error, returnTo }) => {
if (error) setError(error.message);
else router.replace(returnTo ?? "/");
});
}, [router]);
if (error) return <p>Sign-in failed: {error}</p>;
return <p>Signing you in…</p>;
}Returning the user to where they were. Pass
returnTowhen you start the login and it round-trips back to you here — nosessionStoragebookkeeping needed. It is stored locally next to the PKCE verifier and never sent to the server:await faableauth.signInWithOauthConnection({ redirectTo: window.location.origin + "/callback", returnTo: "/dashboard", });
Accessing user state
Anywhere inside the provider, use the useSession and useUser hooks to read the current session and user profile. Call signInWithOauthConnection on the client to start the login, and signOut to clear it.
// app/page.tsx (or pages/index.tsx)
"use client";
import { useSession, useUser } from "@faable/auth-helpers-react";
import { faableauth } from "../lib/faable";
export default function Home() {
const session = useSession();
const user = useUser();
const handleLogin = async () => {
await faableauth.signInWithOauthConnection({
redirectTo: window.location.origin + "/callback",
});
};
const handleLogout = async () => {
await faableauth.signOut();
};
if (!session) {
return (
<div>
<p>You are not logged in.</p>
<button onClick={handleLogin}>Log In</button>
</div>
);
}
return (
<div>
<h1>Welcome, {user?.email}</h1>
<p>Your Access Token: {session.access_token}</p>
<button onClick={handleLogout}>Log Out</button>
</div>
);
}By default,
signInWithOauthConnectionopens your hosted login page and the user picks which connection to use. To skip that picker and force a specific provider (e.g. Google), passconnection: "connection_xxxxxxxxxxxxxxxxxxxxxx"— you’ll find the ID in Dashboard → Auth → Connections.
When handleLogin runs, @faable/auth-js takes care of redirecting the user, generating the PKCE verifier, and exchanging the code for tokens when the user is sent back to your callback URL.
Custom username + password login
If you want to build your own login form instead of using the hosted login page, use signInWithUsernamePassword. This is useful when you have a Database Connection configured on your tenant and want full control over the UI.
Make sure you have a database connection enabled on your Client (Dashboard → Auth → Database). The credentials are submitted directly to Faable Auth’s /usernamepassword/login endpoint, which on success auto-submits a form that completes the OAuth handshake against your callback URL — same end state as signInWithOauthConnection.
// app/login/page.tsx (or pages/login.tsx)
"use client";
import { useState } from "react";
import { faableauth } from "../../lib/faable";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const { error } = await faableauth.signInWithUsernamePassword({
username: email,
password,
redirect_uri: window.location.origin + "/callback",
});
if (error) setError(error.message);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Log in</button>
{error && <p>{error}</p>}
</form>
);
}Parameters:
| Field | Required | Description |
|---|---|---|
username | Yes | The user’s email (or username, depending on your Database Connection’s identifier settings). |
password | Yes | The user’s password. Submitted over TLS to Faable Auth — never store it client-side. |
redirect_uri | No | Overrides the redirect_uri configured on the client. Must be in your Client’s Allowed Callback URLs whitelist. |
state | No | Opaque value round-tripped through the OAuth flow. Useful for CSRF protection or preserving navigation intent. |
On success the user lands on your callback URL with an authorization code that @faable/auth-js exchanges for a session — exactly like the hosted-login flow. On failure, the returned error carries the server message (wrong credentials, blocked user, etc.).
Last updated on