Skip to Content
🔐 Faable AuthQuickstartNext.js Quickstart

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:

  1. 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 as domain below.

    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.link works out of the box. See Custom Domain if you want to set one up later.

  2. 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/callback for local development).

Installation

Install the required Faable packages in your Next.js project:

npm install @faable/auth-js @faable/auth-helpers-react

Initialize 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.

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/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 returnTo when you start the login and it round-trips back to you here — no sessionStorage bookkeeping 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, signInWithOauthConnection opens your hosted login page and the user picks which connection to use. To skip that picker and force a specific provider (e.g. Google), pass connection: "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:

FieldRequiredDescription
usernameYesThe user’s email (or username, depending on your Database Connection’s identifier settings).
passwordYesThe user’s password. Submitted over TLS to Faable Auth — never store it client-side.
redirect_uriNoOverrides the redirect_uri configured on the client. Must be in your Client’s Allowed Callback URLs whitelist.
stateNoOpaque 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

Last updated on