Nuxt Quickstart 💚
Add a complete login experience to a Nuxt 3 app. @faable/auth-js drives the Authorization Code flow with PKCE, stores the session, refreshes tokens, and syncs across tabs.
The one thing to get right in Nuxt: the SDK is client-only. It uses window, localStorage, and PKCE, so it can’t run during server-side rendering. You create it in a client plugin (plugins/faable.client.ts — the .client suffix makes Nuxt run it only in the browser) and expose the session through a useState-backed composable so it survives hydration.
This is still the framework-agnostic core pattern — createClient + onAuthStateChange + getSession — wired into Nuxt’s SSR-safe state. The Vue, JavaScript, SvelteKit, and Angular quickstarts are the same three calls in each framework.
You need one package: @faable/auth-js — Nuxt uses the core client directly, no framework helper required.
✅ Prerequisites
In the Faable Dashboard , create a Client for your app and configure:
- Allowed Callback URLs:
http://localhost:3000/callback(add your production URL later). - Allowed Logout URLs:
http://localhost:3000. - Allowed Web Origins:
http://localhost:3000.
Note your auth domain (your-domain.auth.faable.link) and Client ID. Browser apps are public clients — no client secret is involved.
🛠️ Step 1: Create the App and Install
npm create nuxt@latest my-app # or: npx nuxi init my-app
cd my-app
npm install @faable/auth-jsStep 2: Create the Client in a Client Plugin
The SDK touches window and PKCE storage, so it must never run on the server. A plugin named *.client.ts runs only in the browser — exactly where you want it. Create the client there and provide it to the rest of the app via nuxtApp.provide.
// plugins/faable.client.ts
import { createClient } from "@faable/auth-js";
export default defineNuxtPlugin(() => {
const auth = createClient({
domain: "your-domain.auth.faable.link",
clientId: "YOUR_CLIENT_ID",
redirectUri: window.location.origin + "/callback",
});
return {
provide: { auth },
};
});provide: { auth } exposes the client as useNuxtApp().$auth everywhere in the app. On creation the client recovers an existing session from storage, or — on the callback URL — exchanges the PKCE ?code= for tokens.
Step 3: Wire a Reactive Session Composable
Wrap the client in a useAuth() composable. It holds the session in Nuxt’s useState — SSR-safe shared state that survives hydration — seeds it once with getSession(), and keeps it updated with onAuthStateChange on login, logout, token refresh, or a change in another tab. All of that runs client-side, guarded by import.meta.client.
// composables/useAuth.ts
import type { Session } from "@faable/auth-js";
export function useAuth() {
const { $auth } = useNuxtApp();
const session = useState<Session | null>("faable-session", () => null);
// Only the browser has $auth (client plugin) and window.
if (import.meta.client && $auth) {
// Keep state in sync with every auth change (login, refresh, logout, other tabs).
$auth.onAuthStateChange((_event, next) => {
session.value = next;
});
// Seed the initial value once (also auto-refreshes an expired session).
$auth.getSession().then(({ data }) => {
session.value = data.session;
});
}
return {
auth: $auth,
session,
user: computed(() => session.value?.user ?? null),
signIn: () => $auth?.signInWithOauthConnection({}),
signOut: () => $auth?.signOut({ returnTo: window.location.origin }),
};
}onAuthStateChange returns { data: { subscription } }; call subscription.unsubscribe() if you ever need to tear the listener down. Here it lives for the app’s lifetime, so we let it run.
Step 4: Login, User, and Logout
Any page or component reads the composable. Because sign-in state only exists in the browser, wrap the auth UI in <ClientOnly> — it renders nothing on the server and mounts the real UI after hydration, so there’s no flash of the wrong state.
<!-- app.vue -->
<script setup lang="ts">
const { session, user, signIn, signOut } = useAuth();
</script>
<template>
<ClientOnly>
<button v-if="!session" @click="signIn">Sign in</button>
<div v-else>
<p>Hello {{ user?.email }}</p>
<button @click="signOut">Sign out</button>
</div>
<template #fallback>
<p>Loading…</p>
</template>
</ClientOnly>
</template>signInWithOauthConnection({})sends the user to your tenant’s Universal Login with every connection you’ve enabled. Target one directly with{ connection_id: "connection_..." }.signOut({ returnTo })clears the local session and the SSO cookie on the auth server.returnTomust be in Allowed Logout URLs.- Both methods redirect the browser on success — the promise intentionally never resolves, so don’t put code after the
await. The UI updates through the composable’suseState, not a return value.
Step 5: The Callback Page
Add a /callback page that finishes the exchange, then navigates home. Run handleRedirectCallback() in onMounted so it only fires in the browser. It’s idempotent — the client already started the exchange on creation — and returns { error, returnTo }, so deep links survive the login round-trip if you passed returnTo to signInWithOauthConnection({ returnTo }).
<!-- pages/callback.vue -->
<script setup lang="ts">
const { $auth } = useNuxtApp();
const message = ref("Signing you in…");
onMounted(async () => {
const { error, returnTo } = await $auth.handleRedirectCallback();
if (error) message.value = error.message;
else navigateTo(returnTo ?? "/");
});
</script>
<template>
<p>{{ message }}</p>
</template>Nuxt’s file-based router serves this at /callback automatically — the same path you registered as your Allowed Callback URL. onMounted never runs during SSR, so the SDK stays client-only.
Step 6: Call Your API
The access token lives on the session. Read it fresh before each call — getSession() auto-refreshes an expired session:
// composables/apiFetch.ts
export async function apiFetch(path: string) {
const { $auth } = useNuxtApp();
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
Why a client plugin instead of a normal plugin?
@faable/auth-js uses window, localStorage, and PKCE, none of which exist during Nuxt’s server-side render. A plugin named faable.client.ts runs only in the browser — the .client suffix is Nuxt’s built-in signal. Create the client there and provide it, and it never executes on the server. Guard any composable logic with import.meta.client for the same reason.
How do I keep the session reactive without it breaking hydration?
Hold it in useState<Session | null>("faable-session", () => null) — Nuxt’s SSR-safe shared state — update it from onAuthStateChange, and seed it once with getSession() (Step 3). Every component that calls useAuth() shares the same state, and wrapping auth UI in <ClientOnly> avoids a server/client mismatch since the real session only exists in the browser.
How do I get the access token in Nuxt?
From the session: const { data } = await $auth.getSession(), then data.session?.access_token. There is no separate getAccessToken() — getSession() already refreshes expired tokens before returning. Read it fresh right before each API call, as in Step 6.
How do I send users straight to one provider?
Pass connection_id to signInWithOauthConnection — e.g. signIn: () => $auth.signInWithOauthConnection({ connection_id: "connection_..." }). Find the ID in Dashboard → Auth → Connections. Without it, users pick on the Universal Login screen.
Why doesn’t the sign-in promise resolve?
signInWithOauthConnection and signOut redirect the browser on success, so the page unloads before the promise settles. Let the composable’s useState drive your UI through onAuthStateChange — don’t put post-login logic after the await.
🔗 Related
- Vue · JavaScript · SvelteKit · Angular — the same core client wired into each framework’s reactivity.
- React Quickstart · Next.js Quickstart — with the React helper hooks.
- Authorization Code Flow with PKCE — what the SDK does under the hood.
- Validate Access Tokens — verify these tokens in your backend.
- Connections — enable Google, GitHub, passwordless, and more.
Last updated on