AuthJet
← Docs

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/react

2. Wrap your app

AuthProvider creates the client once.useAuth() tells any component whether the user is signed in.

main.tsx
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.

LoginForm.tsx
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.

Dashboard.tsx
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().

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.

auth.ts
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.

baseUrlRequired. The AuthJet API — https://api.authjet.dev.
tenantSlugDefault tenant for sign-in. Leave it out if one app serves many companies — login then asks which one when an email belongs to several.
storageWhere tokens are kept. localStorage by default; memoryStorage() for server rendering or tests.
autoRefreshRefresh the access token before it expires (default true).
crossTabKeep every open tab signed in and out together (default true).

Good to know