azirid-access
v0.4.0
Published
Authentication components for React and Next.js — Login, Register, powered by TanStack Query and Zod.
Maintainers
Readme
azirid-access
Authentication components and hooks for React and Next.js — powered by TanStack Query and Zod.
Drop-in <LoginForm>, <SignupForm> and more, or use the headless hooks to build fully custom UIs.
Installation
npm install azirid-access
# or
pnpm add azirid-access
# or
yarn add azirid-accessPeer dependencies
npm install react react-dom @tanstack/react-query
# Tailwind CSS is optional – only needed if you use the built-in componentsQuick start
1. Wrap your app with <AziridProvider>
// app/layout.tsx (Next.js App Router) or main.tsx (Vite/CRA)
import { AziridProvider } from 'azirid-access'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<AziridProvider
publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}
tenantId="tenant_abc"
onLoginSuccess={(data) => console.log('Logged in:', data.user)}
onLogoutSuccess={() => console.log('Logged out')}
onSessionExpired={() => (window.location.href = '/login')}
>
{children}
</AziridProvider>
)
}2. Add the built-in forms
import { LoginForm, SignupForm } from 'azirid-access'
// Login page
export default function LoginPage() {
return (
<LoginForm
onSuccess={(data) => {
console.log('User:', data.user)
}}
/>
)
}
// Signup page
export default function SignupPage() {
return <SignupForm />
}Headless hooks
All hooks require <AziridProvider> in the tree.
useAzirid — session state
import { useAzirid } from 'azirid-access'
function Navbar() {
const { user, isAuthenticated, isLoading, login, logout } = useAzirid()
if (isLoading) return <Spinner />
return isAuthenticated ? (
<div>
<span>Hello, {user!.email}</span>
<button onClick={logout}>Sign out</button>
</div>
) : (
<button onClick={() => login({ email: '...', password: '...' })}>Sign in</button>
)
}useLogin
import { useLogin } from 'azirid-access'
function CustomLoginForm() {
const { login, isLoading, error } = useLogin({
onSuccess: (data) => console.log(data.user),
onError: (msg) => console.error(msg),
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
login({
email: fd.get('email') as string,
password: fd.get('password') as string,
})
}}
>
<input name="email" type="email" />
<input name="password" type="password" />
{error && <p>{error}</p>}
<button disabled={isLoading}>Sign in</button>
</form>
)
}useSignup
import { useSignup } from 'azirid-access'
const { signup, isLoading, error } = useSignup({
onSuccess: (data) => console.log('Registered:', data.user),
})
signup({ email: '[email protected]', password: 'secret', name: 'Alice' })useLogout
import { useLogout } from 'azirid-access'
const { logout, isLoading } = useLogout({
onSuccess: () => router.push('/login'),
})useSession
import { useSession } from 'azirid-access'
const { user, accessToken, isAuthenticated } = useSession()useMagicLink
import { useMagicLink } from 'azirid-access'
const { requestMagicLink, verifyMagicLink, isLoading } = useMagicLink()
requestMagicLink({ email: '[email protected]' })
verifyMagicLink({ token: '...' })useSocialLogin
import { useSocialLogin } from 'azirid-access'
const { loginWithProvider, isLoading } = useSocialLogin()
loginWithProvider({ provider: 'google' }) // "google" | "github"usePasskeys
import { usePasskeys } from 'azirid-access'
const { passkeys, registerPasskey, removePasskey, isLoading } = usePasskeys()useChangePassword
import { useChangePassword } from 'azirid-access'
const { changePassword, isLoading, error } = useChangePassword()
changePassword({ currentPassword: 'old', newPassword: 'new' })useBootstrap
Manually re-run the session bootstrap (useful after SSO redirects).
import { useBootstrap } from 'azirid-access'
const { bootstrap, isBootstrapping } = useBootstrap()useRefresh
Manually refresh the access token.
import { useRefresh } from 'azirid-access'
const { refresh } = useRefresh()useAziridClient
Access the raw AccessClient instance for custom API calls.
import { useAziridClient } from 'azirid-access'
function CustomAction() {
const client = useAziridClient()
async function fetchCustomData() {
const data = await client.get('/v1/custom-endpoint')
console.log(data)
}
return <button onClick={fetchCustomData}>Fetch</button>
}useFormState
Headless form hook with Zod validation. Powers the built-in form components — use it to build fully custom forms.
import { useFormState, loginSchema } from 'azirid-access'
function CustomForm() {
const { values, errors, isSubmitting, handleChange, handleSubmit, reset } = useFormState(
{ email: '', password: '' },
loginSchema,
async (values) => {
// Submit logic
},
)
return (
<form onSubmit={handleSubmit}>
<input value={values.email} onChange={handleChange('email')} />
{errors.find((e) => e.field === 'email')?.message}
<button disabled={isSubmitting}>Submit</button>
</form>
)
}usePasswordToggle
Simple toggle between "password" and "text" input types.
import { usePasswordToggle } from 'azirid-access'
function PasswordInput() {
const { visible, toggle, type } = usePasswordToggle()
return (
<div>
<input type={type} name="password" />
<button type="button" onClick={toggle}>
{visible ? 'Hide' : 'Show'}
</button>
</div>
)
}Internationalization (i18n)
Built-in support for English and Spanish. The SDK ships two complete dictionaries; pass a locale prop to switch languages.
import { AziridProvider } from 'azirid-access'
;<AziridProvider publishableKey="pk_live_..." locale="en">
{/* All form labels, validation messages, and UI text render in English */}
{children}
</AziridProvider>Supported locales
| Locale | Language |
| ------ | ----------------- |
| "es" | Spanish (default) |
| "en" | English |
Custom messages
Override any string by passing a partial messages object:
<AziridProvider
publishableKey="pk_live_..."
locale="en"
messages={{
login: { title: 'Welcome back!', submit: 'Sign in' },
validation: { emailRequired: 'Please enter your email' },
}}
>
{children}
</AziridProvider>Using i18n hooks directly
import { useMessages, useBranding } from 'azirid-access'
function CustomForm() {
const msg = useMessages() // resolved messages for current locale
return <label>{msg.login.emailLabel}</label>
}Locale-aware Zod schemas
import { createLoginSchema, createSignupSchema } from 'azirid-access'
// Pass custom validation messages
const schema = createLoginSchema({
emailRequired: 'Email is required',
emailInvalid: 'Must be a valid email',
passwordRequired: 'Password is required',
passwordMin: 'At least 8 characters',
})Branding
The bootstrap endpoint returns branding data configured in the Azirid dashboard (Settings > Branding). The built-in form components automatically apply branding.
Auto-branding from bootstrap
If branding is configured for your app, the forms will automatically:
- Show your logo (from
branding.logoUrl) above the form - Use your display name as the form title
- Apply your primary color to the submit button
- Show/hide the "Secured by Azirid" badge
No extra code needed — just configure branding in the dashboard.
Overriding branding with props
Per-component props always take priority over branding context:
<LoginForm
logo={<MyCustomLogo />} // overrides branding.logoUrl
title="Sign in to Acme" // overrides branding.displayName
submitText="Continue"
/>Using branding hooks
import { useBranding } from 'azirid-access'
function CustomHeader() {
const branding = useBranding() // AppBranding | null
return (
<div>
{branding?.logoUrl && <img src={branding.logoUrl} alt="Logo" />}
<h1 style={{ color: branding?.primaryColor ?? '#000' }}>
{branding?.displayName ?? 'My App'}
</h1>
</div>
)
}"Secured by Azirid" badge
The <SecuredByBadge /> component renders below each form. It's hidden when branding.removeBranding is true (configurable in the dashboard).
import { SecuredByBadge } from "azirid-access";
// Use in custom form layouts
<form>
{/* ... your form fields ... */}
</form>
<SecuredByBadge />createAccessClient
Under the hood AziridProvider creates an AccessClient via createAccessClient. You can also create a client directly to make raw API calls.
import { createAccessClient, PATHS } from 'azirid-access'
import type { AccessClientConfig } from 'azirid-access'
const config: AccessClientConfig = {
baseUrl: 'https://api.azirid.com/v1',
headers: { 'X-Custom': 'value' }, // optional extra headers
}
const client = createAccessClient(config, {
publishableKey: 'pk_live_...',
tenantId: 'tenant_abc', // optional
})
// Set tokens after login
client.setAccessToken('eyJ...')
client.setRefreshToken('...')
// Make arbitrary authenticated calls
const data = await client.get(client.paths.me)
const result = await client.post('/v1/custom-endpoint', { foo: 'bar' })createAccessClient signature
function createAccessClient(
config: AccessClientConfig,
appContext?: { publishableKey: string; tenantId?: string },
): AccessClient| Param | Type | Description |
| ------------ | -------------------- | ------------------------------------------------------- |
| config | AccessClientConfig | { baseUrl: string; headers?: Record<string, string> } |
| appContext | object | Optional. publishableKey and tenantId |
AziridProvider props
| Prop | Type | Default | Description |
| ------------------ | ------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| children | ReactNode | — | Required. Your app tree |
| publishableKey | string | — | Publishable key (e.g. pk_live_...) |
| tenantId | string | — | Tenant ID for multi-tenant apps |
| fetchOptions | Record<string, string> | — | Extra headers to send with every request |
| autoBootstrap | boolean | true | Auto-restore session on mount |
| refreshInterval | number | 50000 | Token refresh interval in ms. 0 to disable |
| sessionSyncUrl | string \| false | auto | URL for session cookie sync. Auto-activates in dev mode. Pass false to disable |
| onLoginSuccess | (data) => void | — | Called after successful login |
| onSignupSuccess | (data) => void | — | Called after successful signup |
| onLogoutSuccess | () => void | — | Called after logout |
| onSessionExpired | () => void | — | Called when refresh fails |
| onError | (msg: string) => void | — | Called on any auth error |
| locale | "es" \| "en" | "es" | UI language for built-in forms and validation messages |
| messages | Partial<AccessMessages> | — | Override any i18n string (merged on top of the locale dictionary) |
Next.js Integration
azirid-access supports Next.js 14, 15, and 16+ with full compatibility for each version's API conventions.
Proxy Route Handler (all versions)
Create the file app/v1/auth/[...path]/route.ts — one line is all you need:
// app/v1/auth/[...path]/route.ts
export { GET, POST, PUT, PATCH, DELETE } from 'azirid-access/next'That's it. The library handles all the proxy logic, cookie fixing, and header forwarding internally. It works with Next.js 14, 15, and 16+ automatically.
If you need a custom API URL or debug logging:
// app/v1/auth/[...path]/route.ts
import { createAziridRouteHandlers } from 'azirid-access/next'
export const { GET, POST, PUT, PATCH, DELETE } = createAziridRouteHandlers({
apiUrl: 'https://my-custom-api.com',
debug: true, // logs proxy requests to console
})Next.js Config
Next.js 16+ (next.config.ts)
Turbopack is the default bundler — transpilePackages is no longer needed:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {}
export default nextConfigNext.js 14/15 (next.config.js)
// next.config.js
const { withAziridProxy } = require('azirid-access/next')
/** @type {import('next').NextConfig} */
module.exports = withAziridProxy()({
transpilePackages: ['azirid-access'],
})Route Protection (optional)
Not required for basic usage. Only add this if you need to protect specific routes from unauthenticated users.
Next.js 16+ (proxy.ts)
// proxy.ts — only needed if you want route protection
import { createAziridProxy } from 'azirid-access/next'
export const proxy = createAziridProxy({
protectedRoutes: ['/dashboard', '/settings'],
loginUrl: '/login',
publicRoutes: ['/login', '/signup', '/forgot-password'],
})
export const config = {
matcher: ['/((?!_next|favicon.ico|api/).*)'],
}Next.js 14/15 (middleware.ts)
// middleware.ts — only needed if you want route protection
import { createAziridMiddleware } from 'azirid-access/next'
export default createAziridMiddleware({
protectedRoutes: ['/dashboard', '/settings'],
loginUrl: '/login',
publicRoutes: ['/login', '/signup', '/forgot-password'],
})
export const config = {
matcher: ['/((?!_next|favicon.ico|api/).*)'],
}Server-side (Next.js App Router)
For Server Components, Server Actions, and Route Handlers, use the azirid-access/server entry point to read the session token from the httpOnly __session cookie.
Setup
// lib/access-server.ts
import { cookies } from 'next/headers'
import { createServerAccess } from 'azirid-access/server'
// Works with all Next.js versions:
// - Next.js 14: cookies() returns a sync cookie store
// - Next.js 15/16+: cookies() returns a Promise — handled automatically
export const { getSessionToken, getAccessToken } = createServerAccess({ cookies })Server Action example
// app/actions/profile.ts
'use server'
import { getSessionToken } from '@/lib/access-server'
export async function getProfile() {
const token = await getSessionToken()
if (!token) throw new Error('Not authenticated')
const res = await fetch(`${process.env.API_URL}/v1/users/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}Server Component example
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { getSessionToken } from '@/lib/access-server'
export default async function DashboardPage() {
const token = await getSessionToken()
if (!token) redirect('/login')
const res = await fetch(`${process.env.API_URL}/v1/users/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
})
const user = await res.json()
return <h1>Hello, {user.email}</h1>
}Options
| Option | Type | Default | Description |
| ------------ | -------- | ------------- | ------------------------------------------ |
| cookieName | string | "__session" | Name of the httpOnly cookie with the token |
createSessionSyncHandler
Creates a route handler that syncs the access token to a local httpOnly cookie. Useful for cross-origin development setups where the API is on a different domain.
// app/api/auth/session/route.ts
import { createSessionSyncHandler } from 'azirid-access/server'
export const { POST, DELETE } = createSessionSyncHandler()With custom options:
export const { POST, DELETE } = createSessionSyncHandler({
cookieName: '__session', // default
secure: true, // set Secure flag on cookie
maxAge: 3600, // cookie max age in seconds (default: 1h)
})| Option | Type | Default | Description |
| ------------ | --------- | ------------- | ----------------------------------- |
| cookieName | string | "__session" | Name of the httpOnly cookie |
| secure | boolean | false | Set the Secure flag on the cookie |
| maxAge | number | 3600 | Cookie max age in seconds |
Version Compatibility
| Feature | Next.js 14 | Next.js 15 | Next.js 16+ |
| ------------------- | ---------------- | ------------------- | ------------------- |
| React | 18.x | 18.x / 19.x | 19.x+ |
| Node.js | >= 18.0.0 | >= 18.17.0 | >= 20.9.0 |
| Config file | next.config.js | next.config.js/ts | next.config.ts |
| Request interceptor | middleware.ts | middleware.ts | proxy.ts |
| cookies() | sync | async (with compat) | async only |
| params | sync | async (with compat) | async only |
| Bundler | Webpack | Webpack/Turbopack | Turbopack (default) |
| transpilePackages | Required | Required | Not needed |
Tailwind CSS (optional)
The built-in form components (LoginForm, SignupForm, etc.) use Tailwind utility classes. Add azirid-access to your content glob so Tailwind picks them up.
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{ts,tsx}', './node_modules/azirid-access/dist/**/*.{js,mjs}'],
}License
MIT © Azirid
