JavaScript Quickstart 🟨
Add a complete login experience to a plain JavaScript single-page app — no framework. @faable/auth-js drives the Authorization Code flow with PKCE, stores the session, refreshes tokens, and syncs across tabs. You wire it to the DOM with one subscription.
This is the framework-agnostic core pattern: createClient + onAuthStateChange + getSession. The Vue, SvelteKit, and Angular quickstarts are the same three calls wired into each framework’s reactivity.
You need one package: @faable/auth-js — no framework helper required.
✅ 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 vanilla-ts
cd my-app
npm install @faable/auth-jsStep 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: Render on Auth State
Subscribe once with onAuthStateChange and re-render whenever the session changes — login, logout, token refresh, or a change in another tab. getSession() returns the current session (and auto-refreshes it if expired).
// src/main.ts
import { auth } from './auth'
const app = document.querySelector<HTMLDivElement>('#app')!
async function render() {
// On the callback route, finish the login and go home (Step 5)
if (location.pathname === '/callback') {
const { error, returnTo } = await auth.handleRedirectCallback()
if (error)
return void (app.textContent = `Sign-in failed: ${error.message}`)
return void location.replace(returnTo ?? '/')
}
const { data } = await auth.getSession()
if (!data.session) {
app.innerHTML = `<button id="login">Sign in</button>`
document
.querySelector('#login')!
.addEventListener('click', () => auth.signInWithOauthConnection({}))
return
}
app.innerHTML = `
<p>Hello ${data.session.user.email}</p>
<button id="logout">Sign out</button>`
document
.querySelector('#logout')!
.addEventListener('click', () =>
auth.signOut({ returnTo: location.origin })
)
}
// Re-render on every auth change, then do the first render.
auth.onAuthStateChange(() => render())
render()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.
Step 4: Add the Callback Route
The render() above already handles /callback by calling handleRedirectCallback(). In a Vite SPA, make sure a request to /callback serves the same index.html (add a rewrite in vite.config.ts, or use hash routing). handleRedirectCallback() awaits the code-for-tokens exchange (it’s idempotent — the client already started it) and returns { error, returnTo }, so deep links survive the login round-trip if you passed returnTo to signInWithOauthConnection({ returnTo }).
Step 5: Call Your API
The access token lives on the session. Read it fresh before each call — getSession() auto-refreshes an expired session:
// src/api.ts
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 in plain JavaScript?
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.
Do I need a framework or a build step?
No. @faable/auth-js is framework-agnostic and works with any bundler (Vite here) or plain ES modules. The React, Vue, SvelteKit, and Angular quickstarts wire the same client into each framework.
How do I send users straight to one provider?
Pass connection_id to signInWithOauthConnection — e.g. { 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. Put post-login logic in the onAuthStateChange handler, not after the await.
🔗 Related
- Vue · SvelteKit · Angular — the same pattern in a framework.
- 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