Sign users in with the JavaScript SDK
Build your own login screen and let the SDK handle the rest: storing tokens, refreshing them before they expire, keeping tabs in sync, and attaching them to calls to your API.
@authjet/react
A provider and hooks for React 18+. Start here if you use React.
@authjet/core
The same client with no framework — Vue, Svelte, plain JavaScript.
1. Install
npm install @authjet/react2. Wrap your app
AuthProvider creates the client once.useAuth() tells any component whether the user is signed in.
import { createRoot } from 'react-dom/client'
import { AuthProvider, useAuth } from '@authjet/react'
import { LoginForm } from './LoginForm'
import { Dashboard } from './Dashboard'
function App() {
const { isAuthenticated } = useAuth() // re-renders on sign-in, sign-out and refresh
return isAuthenticated ? <Dashboard /> : <LoginForm />
}
createRoot(document.getElementById('root')!).render(
<AuthProvider config={{ baseUrl: 'https://api.authjet.dev' }}>
<App />
</AuthProvider>,
)3. Build your login form
client.login() returns one of three results:success (you're done), tenant_selection (the email belongs to more than one company — ask which), or mfa_required (ask for their 6-digit code). Failures throwAuthApiError with a readabledetail.
import { useState } from 'react'
import { AuthApiError, useAuthClient, type TenantOption } from '@authjet/react'
export function LoginForm() {
const client = useAuthClient()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [tenants, setTenants] = useState<TenantOption[]>([])
const [challengeToken, setChallengeToken] = useState<string | null>(null)
const [code, setCode] = useState('')
const [error, setError] = useState<string | null>(null)
async function run(action: () => Promise<unknown>) {
setError(null)
try {
await action()
} catch (e) {
setError(e instanceof AuthApiError ? e.detail : 'Could not reach the server')
}
}
const signIn = (tenantSlug?: string) =>
run(async () => {
const result = await client.login({ email, password, tenantSlug })
if (result.status === 'tenant_selection') setTenants(result.tenants) // email is in several companies
if (result.status === 'mfa_required') setChallengeToken(result.challengeToken)
// 'success': tokens are saved and useAuth() flips to signed in — nothing else to do
})
if (challengeToken) {
return (
<form onSubmit={(e) => { e.preventDefault(); run(() => client.verifyMfa({ challengeToken, code })) }}>
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="6-digit code" />
<button>Verify</button>
{error && <p>{error}</p>}
</form>
)
}
if (tenants.length > 0) {
return (
<div>
<p>Which company?</p>
{tenants.map((t) => (
<button key={t.slug} onClick={() => signIn(t.slug)}>{t.name}</button>
))}
</div>
)
}
return (
<form onSubmit={(e) => { e.preventDefault(); signIn() }}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
<button>Sign in</button>
{error && <p>{error}</p>}
</form>
)
}4. Call your API and sign out
useAuthFetch() sends the user's access token to your backend. On the backend, verify it before trusting it.
import { useEffect, useState } from 'react'
import { useAuth, useAuthFetch } from '@authjet/react'
export function Dashboard() {
const { client } = useAuth()
const authFetch = useAuthFetch() // adds "Authorization: Bearer …" and retries once after a refresh on 401
const [bookings, setBookings] = useState<unknown[]>([])
useEffect(() => {
authFetch('https://api.your-app.com/bookings')
.then((res) => res.json())
.then(setBookings)
}, [authFetch])
return (
<div>
<p>{bookings.length} bookings</p>
<button onClick={() => client.logout()}>Sign out</button>
</div>
)
}Other sign-in flows
All are methods on the client from useAuthClient().
Self sign-up
signup → verifySignupCreate an account; we email a link to your page, which passes the token to verifySignup.
Magic link
requestMagicLink → consumeMagicLinkPasswordless: email a one-time link, then exchange its token for a session.
Forgot password
requestPasswordReset → confirmPasswordResetEmail a reset link to your page, then set the new password.
Accept an invite
acceptInviteAn invited user sets their password from the link in their invite email.
Two-factor
verifyMfaWhen login returns 'mfa_required', send the user's 6-digit code with the challenge token.
Hooks
useAuth() | { isAuthenticated, tokens, accessToken, client } — re-renders on sign-in, sign-out, refresh and changes from other tabs. |
useAuthFetch() | fetch with the Bearer token attached, and one refresh-and-retry on a 401. |
useAccessToken() | The current access token, or null. |
useAuthEvent(event, handler) | Subscribe to 'tokensChanged' or 'refreshError' for the component's lifetime. |
useAuthClient() | The client itself — every auth action (login, signup, logout, …) is a method on it. |
Without React
npm install @authjet/core — the same client and methods, no provider.
import { createAuthClient } from '@authjet/core'
const auth = createAuthClient({ baseUrl: 'https://api.authjet.dev', tenantSlug: 'your-tenant' })
// React to sign-in, refresh and sign-out (including from other tabs).
auth.on('tokensChanged', (tokens) => {
document.body.dataset.signedIn = String(tokens !== null)
})
export async function signIn(email: string, password: string) {
const result = await auth.login({ email, password })
if (result.status !== 'success') throw new Error(`Needs another step: ${result.status}`)
}
export const callMyApi = (path: string) => auth.authFetch(`https://api.your-app.com${path}`)
export const signOut = () => auth.logout()Options
Passed as config to AuthProvider, or to createAuthClient.
baseUrl | Required. The AuthJet API — https://api.authjet.dev. |
tenantSlug | Default tenant for sign-in. Leave it out if one app serves many companies — login then asks which one when an email belongs to several. |
storage | Where tokens are kept. localStorage by default; memoryStorage() for server rendering or tests. |
autoRefresh | Refresh the access token before it expires (default true). |
crossTab | Keep every open tab signed in and out together (default true). |
Good to know
- Access tokens last 30 minutes; the SDK refreshes them in the background, so users stay signed in as long as they come back within 7 days.
- Sign-out clears the session in the browser. Refresh tokens are single-use and expire on their own.
- On a server-rendered page, useAuth() reports signed-out until the browser takes over — show a loading state for sign-in-only UI.
AuthJet