azirid-access
v0.6.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 componentsArchitecture: Proxy vs Direct Mode
azirid-access supports two connection modes to the Azirid API. Choose the one that fits your stack:
| | Proxy mode | Direct mode |
| --- | --- | --- |
| Best for | Next.js (App Router) | React SPA (Vite, CRA, Remix) |
| Security | First-party cookies, no CORS | Requires CORS on API |
| Setup | Route handler + Provider | Provider only |
| How it works | Browser → your app /api/auth/* → Azirid API | Browser → Azirid API directly |
Proxy mode (recommended for Next.js)
Requests go to your Next.js app's /api/auth/* route handler, which securely proxies them to the Azirid API. Cookies are first-party (same domain), so no CORS configuration is needed.
// No apiUrl prop → proxy mode is activated automatically
<AziridProvider publishableKey="pk_live_...">Direct mode (for React SPA / Vite)
Requests go directly to the Azirid API. Requires CORS to be configured on the API server.
// apiUrl prop → direct mode
<AziridProvider apiUrl="https://api.azirid.com" publishableKey="pk_live_...">Quick Start — Next.js (Proxy Mode)
1. Create the route handler
// app/api/auth/[...path]/route.ts
export { GET, POST, PUT, PATCH, DELETE } from 'azirid-access/next'That's it. The library handles proxy logic, cookie fixing, and header forwarding internally. Works with Next.js 14, 15, and 16+ automatically.
2. Set the API URL (optional)
# .env (server-side only — never exposed to the browser)
# Default: https://api.azirid.com
# For local development with the API running locally:
AZIRID_API_URL=http://localhost:30003. Wrap your app with <AziridProvider>
// app/layout.tsx
import { AziridProvider } from 'azirid-access'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<AziridProvider
publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}
onLoginSuccess={(data) => console.log('Logged in:', data.user)}
onLogoutSuccess={() => console.log('Logged out')}
onSessionExpired={() => (window.location.href = '/login')}
>
{children}
</AziridProvider>
)
}4. Add the login page
// app/login/page.tsx
import { LoginForm } from 'azirid-access'
export default function LoginPage() {
return <LoginForm />
}Quick Start — React SPA (Direct Mode)
1. Wrap your app with <AziridProvider>
// main.tsx (Vite / CRA)
import { AziridProvider } from 'azirid-access'
createRoot(document.getElementById('root')!).render(
<AziridProvider
apiUrl={import.meta.env.VITE_AZIRID_API_URL || 'https://api.azirid.com'}
publishableKey={import.meta.env.VITE_AZIRID_PK}
onLoginSuccess={(data) => console.log('Logged in:', data.user)}
>
<App />
</AziridProvider>,
)2. Configure your environment
# .env
VITE_AZIRID_API_URL=https://api.azirid.com
VITE_AZIRID_PK=pk_live_...3. Use the forms or hooks
import { LoginForm } from 'azirid-access'
export default function LoginPage() {
return <LoginForm />
}No route handler or proxy needed — requests go directly to the API.
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' })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, BASE_PATHS } from 'azirid-access'
import type { AccessClientConfig } from 'azirid-access'
// Direct mode — point to the API
const client = createAccessClient(
{
baseUrl: 'https://api.azirid.com',
basePath: BASE_PATHS.direct, // '/v1/users/auth'
},
{ publishableKey: 'pk_live_...' },
)
// 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, basePath?, headers? } |
| appContext | object | Optional. publishableKey and tenantId |
AziridProvider props
| Prop | Type | Default | Description |
| ------------------ | ------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| children | ReactNode | — | Required. Your app tree |
| apiUrl | string | — | API URL for direct mode. Omit for proxy mode (recommended in Next.js) |
| 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/api/auth/[...path]/route.ts — one line is all you need:
// app/api/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.
Custom API URL or debug logging
// app/api/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
})Environment variable
The proxy reads AZIRID_API_URL (server-side only) to know where to forward requests:
# .env
# Default: https://api.azirid.com
# For local development:
AZIRID_API_URL=http://localhost:3000Important: Use
AZIRID_API_URL(withoutNEXT_PUBLIC_prefix) — the API URL should never be exposed to the browser. The proxy runs server-side only.
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.AZIRID_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.AZIRID_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
