An isomorphic JavaScript client for Faable Auth.
π Full documentation at faable.com/docs
- OAuth social connections (Google, GitHub, β¦) with PKCE and implicit flows
- Username + password login
- Passwordless: email magic link and OTP code
- Automatic token refresh with cross-tab synchronization via
BroadcastChannel - Pluggable storage adapters (
localStorage, cookies, or custom) - Server-side session helpers for Next.js
npm install @faable/auth-jsRequires Node.js >=22.8 for development. The published bundle runs in any
modern browser and in Node/SSR environments.
import { createClient } from '@faable/auth-js'
export const auth = createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
redirectUri: window.location.origin
})
// Trigger a social login
await auth.signInWithOauthConnection({ connection: 'google' })createClient(config) accepts:
| Option | Type | Description |
|---|---|---|
domain |
string |
Required. Your Faable Auth tenant domain. |
clientId |
string |
Required. Application client ID. |
redirectUri |
string |
Default callback URL. Falls back to window.location.origin. |
scope |
string |
Space-separated scopes. Defaults to openid profile email. |
storage |
SupportedStorage |
Custom storage adapter. Defaults to localStorage. |
storageKey |
string |
Prefix for the storage key. Final key is ${storageKey}-${clientId}. |
cookieOptions |
CookieOptions |
When set, switches storage to the cookie adapter. |
lock |
LockFunc |
Custom locking primitive for concurrent refreshes. |
debug |
boolean |
Enables verbose logging. |
// Use the default connection configured on the tenant
await auth.signInWithOauthConnection({})
// Or pick a specific provider (by name or connection_id)
await auth.signInWithOauthConnection({
connection_id: 'conn_01HXβ¦', // preferred when known; falls back to `connection` for legacy tenants
redirectTo: 'https://app.example.com/callback',
scopes: 'openid profile email',
queryParams: { prompt: 'select_account' }
})In browsers the SDK uses the PKCE flow by default and exchanges the code for a
session on the callback page. The first call to createClient automatically
processes the URL when the user lands back on the redirect target.
await auth.signInWithUsernamePassword({
username: 'user@example.com',
password: 'β’β’β’β’β’β’β’β’',
redirectTo: 'https://app.example.com/callback'
})// Step 1 β request a code or link
await auth.signInWithPasswordless({
email: 'user@example.com',
type: 'code' // or "link"
})
// Step 2 β complete the login with the OTP the user received
const { data, error } = await auth.signInWithOtp({
username: 'user@example.com',
otp: '123456'
})await auth.changePassword({ email: 'user@example.com' })await auth.signOut() // global β all sessions for this user
await auth.signOut({ scope: 'local' }) // only this device// Get the current session (refreshes if needed)
const {
data: { session }
} = await auth.getSession()
// Subscribe to auth events
const {
data: { subscription }
} = auth.onAuthStateChange((event, session) => {
// event: INITIAL_SESSION | SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED | PASSWORD_RECOVERY | USER_UPDATED
})
// Stop listening
subscription.unsubscribe()
// Force a refresh
await auth.refreshSession()Auth events are broadcast across tabs using BroadcastChannel, so a sign-in or
sign-out in one tab is reflected in every other tab using the same storageKey.
Refresh tokens are sensitive: anyone who reads them can impersonate the user until the token is revoked. The storage you pick decides where they live:
localStorage(default) β simple and supports cross-tab sync viaBroadcastChannel, but any script running on the same origin can read it. A single XSS lets an attacker exfiltrate the refresh token. Acceptable for low-risk apps and prototypes; not recommended when the surface has third-party scripts, user-generated HTML, or strict compliance requirements.- Cookies β required for SSR (server reads them on every request) and the
only adapter that lets you scope storage with
Secure,SameSite, andDomain. Note that this library writes cookies from JavaScript, so they cannot be markedHttpOnly; an XSS can still read them, but cookies make CSRF and same-site policies enforceable in a waylocalStoragedoes not. - Custom adapter β use for in-memory storage (tokens lost on reload, safest against XSS), Web Workers, or platform-specific keychains.
If your app is exposed to untrusted content, prefer cookies with Secure: true
and SameSite: "Lax" (or "Strict"), and treat XSS prevention (CSP, escaping,
framework guarantees) as a hard requirement regardless of which adapter you
pick.
Used automatically in browsers. No configuration required.
Useful for SSR setups where the server must read the session from the request.
import { createClient } from '@faable/auth-js'
export const auth = createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
storage: 'cookie'
})That's it. The adapter sets sensible defaults: Path=/, SameSite=Lax, auto
Secure on HTTPS, and a 30-day Max-Age so users stay signed in across browser
restarts.
Use cookieOptions only when you need to override something β e.g. share the
session across subdomains:
createClient({
domain: '<faableauth_domain>',
clientId: '<client_id>',
storage: 'cookie',
cookieOptions: { domain: '.example.com' }
})Provide any object that implements getItem, setItem, and removeItem (sync
or async). Set isServer: true if values may come from an untrusted source such
as request cookies.
const memoryStorage = {
store: new Map<string, string>(),
getItem: (k: string) => memoryStorage.store.get(k) ?? null,
setItem: (k: string, v: string) => void memoryStorage.store.set(k, v),
removeItem: (k: string) => void memoryStorage.store.delete(k)
}
createClient({ domain, clientId, storage: memoryStorage })Use cookie storage on the client, then read the session from next/headers on
the server:
// app/page.tsx
import { cookies } from 'next/headers'
import { getSessionFromCookies } from '@faable/auth-js'
export default async function Page() {
const session = getSessionFromCookies(cookies(), { clientId: '<client_id>' })
if (!session) return <SignIn />
return <Dashboard user={session.user} />
}Pass the same clientId you used in createClient. If you also passed a custom
storageKey to createClient, mirror it here as { clientId, storageKey } so
the helper looks at the same cookie.
For the full guides, API reference, and dashboard setup walkthroughs visit faable.com/docs.
See LICENSE.md.