azirid-react
v0.24.1
Published
Authentication components for React and Next.js — Login, Register, powered by TanStack Query and Zod.
Downloads
610
Maintainers
Readme
azirid-react
Authentication, billing, and referral components and hooks for React and Next.js — powered by TanStack Query and Zod.
Drop-in <LoginForm>, <SignupForm>, <PayButton> and more, or use the headless hooks to build fully custom UIs.
Installation
npm install azirid-react
# or
pnpm add azirid-react
# or
yarn add azirid-reactPeer dependencies
npm install react react-dom @tanstack/react-queryNo Tailwind, no CSS import, no extra setup required. The built-in auth forms (
<LoginForm>,<SignupForm>,<ForgotPasswordForm>,<ResetPasswordForm>) render inside a Shadow DOM since v0.21 — their styles are fully isolated from your app and ship compiled inside the component. Drop them into any project, any framework config, any CSS reset — they will look identical and nothing from your host stylesheet can leak in.
Quick Start — Next.js (App Router)
1. Create a client wrapper for the provider
<AziridProvider> is a client component. In the App Router the root layout.tsx
is a server component, so you need a 'use client' wrapper that you can mount
from the server layout.
// app/providers.tsx
'use client'
import { AziridProvider } from 'azirid-react'
import type { ReactNode } from 'react'
export function AziridClientProvider({ children }: { children: ReactNode }) {
return (
<AziridProvider publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}>
{children}
</AziridProvider>
)
}2. Mount it from the root layout
// app/layout.tsx — server component, no "use client"
import { AziridClientProvider } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AziridClientProvider>{children}</AziridClientProvider>
</body>
</html>
)
}Why the wrapper? Importing
<AziridProvider>directly into a server layout works in most cases, but the wrapper makes the client boundary explicit and prevents subtle hydration bugs when the layout grows.
Note: You do not need to
import 'azirid-react/styles.css'. The auth forms carry their own Shadow DOM + compiled CSS. The./styles.cssexport still exists for the few light-DOM components (<PricingCard>,<PayButton>, etc.), but<AziridProvider>auto-injects those styles at mount too — so you can forget about it.
3. Protect routes with <AuthGuard> and <GuestGuard>
Because the SDK calls api.azirid.com cross-origin, auth cookies live on the
Azirid domain — not on yours — so server-side middleware can't gate routes.
Use the client-side guards shipped under azirid-react/next/guards instead.
// app/(protected)/layout.tsx — wraps /dashboard, /admin, etc.
import { AuthGuard } from 'azirid-react/next/guards'
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
return <AuthGuard>{children}</AuthGuard>
}// app/(auth)/layout.tsx — wraps /login, /register, /forgot-password, etc.
import { GuestGuard } from 'azirid-react/next/guards'
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <GuestGuard>{children}</GuestGuard>
}AuthGuard waits for bootstrap, then redirects unauthenticated users to
/login?redirect=<original-path>. GuestGuard does the inverse and honors the
?redirect= param so users land back where they came from after logging in.
4. Add the login page
// app/login/page.tsx
import { LoginForm } from 'azirid-react'
export default function LoginPage() {
return <LoginForm />
}5. Configure your environment
# .env.local
NEXT_PUBLIC_AZIRID_PK=pk_live_...Restart the dev server after adding or changing NEXT_PUBLIC_* variables so
Next.js picks them up.
Troubleshooting — login submit does nothing
If the form submits but no request appears in the Network tab:
- Confirm
<AziridProvider>actually wraps the page containing<LoginForm>. Missing provider ⇒<LoginForm>throws a descriptive error in development (added in 0.20.2). - Confirm
NEXT_PUBLIC_AZIRID_PKis set and the dev server was restarted. Missing publishable key ⇒ the provider logs aconsole.erroron mount. - Check the browser console for a
useAziridwarning — it pinpoints a provider/component mismatch.
Cross-origin session handling (zero-config from 0.20.4)
Most consumers of azirid-react run their app on their own domain (e.g.
myapp.com) while talking to api.azirid.com. Modern browsers — Safari ITP,
Firefox ETP, Chrome Privacy Sandbox — treat cookies set by api.azirid.com as
third-party and block them by default, which would break session bootstrap.
The SDK handles this automatically. At mount it inspects apiUrl vs.
window.location.host and picks the right transport:
| Layout | SDK behavior | Where the refresh token lives |
|---|---|---|
| myapp.com → api.azirid.com | body-carried refresh token | In-memory + sent in request body |
| app.acme.com → api.acme.com | HttpOnly cookie (default) | _asid cookie |
| localhost:3001 → localhost:3000 | HttpOnly cookie (default) | _asid cookie |
No flag is required. If you want to override the auto-detection (e.g. for an
unusual reverse-proxy setup) you can still pass crossOriginMode explicitly:
// Force body-RT
<AziridProvider publishableKey={…} crossOriginMode>…</AziridProvider>
// Force cookies (and accept that third-party browsers will break)
<AziridProvider publishableKey={…} crossOriginMode={false}>…</AziridProvider>Under the hood, when the SDK picks the body-carried mode:
- The login response includes the refresh token in the body.
- The SDK persists it to
sessionStorageso the session survives full-page reloads (window.location.href = "/dashboard"after login, F5, etc.). bootstrap,refresh, andlogoutsend{ rt: "…" }in the POST body.- The backend's CSRF guard accepts the body token as proof of session when the cookie is missing — no extra server-side configuration required.
sessionStorageis scoped to the tab, cleared automatically when the tab closes, and isolated per origin. Tokens rotate on every refresh and are bound to the device-id.
Quick Start — React SPA (Vite)
1. Wrap your app with <AziridProvider>
// main.tsx
import { AziridProvider } from 'azirid-react'
createRoot(document.getElementById('root')!).render(
<AziridProvider publishableKey={import.meta.env.VITE_AZIRID_PK}>
<App />
</AziridProvider>,
)2. Configure your environment
VITE_AZIRID_PK=pk_live_...3. Use the forms or hooks
import { LoginForm } from 'azirid-react'
export default function LoginPage() {
return <LoginForm />
}Google sign-in (zero-config from 0.23)
Google sign-in is configured per App by the customer admin in Dashboard → Social Providers. Once enabled, <LoginForm> and <AuthForm> automatically render a "Continue with Google" button — no SDK code changes required.
What the integrator (you) needs to do
Add <OAuthCallbackHandler /> somewhere inside <AziridProvider>. It detects ?ticket=… in the URL after the Google round-trip, exchanges the ticket for session tokens, strips the URL params, and triggers a hard reload so the provider re-bootstraps with the fresh session. It is silent when the URL has no OAuth params, so it's safe to mount once at the app shell.
'use client'
import { AziridProvider, OAuthCallbackHandler } from 'azirid-react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AziridProvider publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}>
<OAuthCallbackHandler />
{children}
</AziridProvider>
)
}That's it. The button appears automatically the moment your customer admin enables Google in the dashboard, and clicking it walks the user through the full Authorization Code + PKCE flow without any client-side OAuth library.
What the customer admin needs to do (one time, per App)
- Open https://console.cloud.google.com/apis/credentials and create an OAuth 2.0 Client ID of type "Web application".
- On the OAuth Consent Screen, register the scopes:
openid,email,profile. - Under Authorized redirect URIs, paste the value the dashboard shows you on the Social Providers page. It looks like
https://api.azirid.com/v1/oauth/google/callback. - Copy the Client ID and Client Secret from Google and paste them into the dashboard. Toggle Enable Google sign-in and save.
Customizing the button
If the built-in button doesn't fit your design, opt out by passing showSocialButtons={false} and build your own using useOAuthStartUrl:
import { LoginForm, useOAuthStartUrl } from 'azirid-react'
function CustomLogin() {
const googleUrl = useOAuthStartUrl('google')
return (
<>
<button onClick={() => googleUrl && (window.location.href = googleUrl)}>
Continue with Google
</button>
<LoginForm showSocialButtons={false} />
</>
)
}Forcing or hiding the auto-render
showSocialButtons accepts three states:
- omitted (default) — the SDK asks the API which providers are enabled and renders only those.
true— render the buttons regardless of API state. Useful in design previews.false— never render the buttons. Useful when you want a fully custom layout.
Components
LoginForm / SignupForm
See Quick Start examples above.
ForgotPasswordForm
Pre-built form that sends a password reset email.
import { ForgotPasswordForm } from 'azirid-react'
export default function ForgotPasswordPage() {
return <ForgotPasswordForm onSubmit={(data) => console.log('Reset requested for:', data.email)} />
}| Prop | Type | Default | Description |
| --------------- | ----------------------------------- | -------- | --------------------------------- |
| onSubmit | (data: { email: string }) => void | — | Override default submit behavior |
| schema | ZodType | auto | Custom Zod schema for validation |
| isLoading | boolean | — | Show loading state / disable form |
| error | string \| null | — | Error message to display |
| title | string | i18n | Form title |
| description | string | i18n | Subtitle below the title |
| logo | ReactNode | branding | Logo above the card |
| submitText | string | i18n | Submit button text |
| footer | ReactNode | — | Content below the form |
| defaultValues | { email?: string } | — | Pre-fill the email field |
| className | string | — | Additional CSS classes |
ResetPasswordForm
Pre-built form for confirming a password reset with a token.
import { ResetPasswordForm } from 'azirid-react'
export default function ResetPasswordPage({ searchParams }: { searchParams: { token: string } }) {
return <ResetPasswordForm token={searchParams.token} onSuccess={() => router.push('/login')} />
}| Prop | Type | Default | Description |
| ------------- | -------------------------------------------------------- | -------- | -------------------------------------- |
| token | string | — | Required. Reset token from the URL |
| onSubmit | (data: { token: string; newPassword: string }) => void | — | Override default submit |
| schema | ZodType | auto | Custom Zod schema |
| isLoading | boolean | — | Show loading state |
| error | string \| null | — | Error message |
| title | string | i18n | Form title |
| description | string | i18n | Subtitle |
| logo | ReactNode | branding | Logo above the card |
| submitText | string | i18n | Submit button text |
| footer | ReactNode | — | Content below the form |
| onSuccess | () => void | — | Called after successful reset |
| className | string | — | Additional CSS classes |
AuthForm
Multi-view auth component that wraps LoginForm, SignupForm, ForgotPasswordForm, and ResetPasswordForm with built-in navigation between views.
import { AuthForm } from 'azirid-react'
// Uncontrolled — manages its own view state
<AuthForm defaultView="login" />
// Controlled — parent manages the view
const [view, setView] = useState<AuthView>('login')
<AuthForm view={view} onViewChange={setView} />| Prop | Type | Default | Description |
| --------------------- | --------------------------------------- | --------- | -------------------------------------------------------------------------------- |
| view | AuthView | — | Controlled view ('login' \| 'signup' \| 'forgot-password' \| 'reset-password') |
| onViewChange | (view: AuthView) => void | — | Callback when navigation links are clicked (controlled mode) |
| defaultView | AuthView | 'login' | Initial view for uncontrolled mode |
| logo | ReactNode | — | Logo passed to all child forms |
| showSocialButtons | boolean | auto | Force-show or hide social-login buttons. When omitted, the form asks the API which providers the customer admin enabled in Dashboard → Social Providers and renders only those (currently Google). |
| hideNavigation | boolean | — | Hide navigation links between views |
| resetToken | string | — | Token for the reset-password view |
| defaultValues | { email?: string; password?: string } | — | Default values passed to child forms |
| loginProps | Partial<LoginFormProps> | — | Props forwarded to LoginForm |
| signupProps | Partial<SignupFormProps> | — | Props forwarded to SignupForm |
| forgotPasswordProps | Partial<ForgotPasswordFormProps> | — | Props forwarded to ForgotPasswordForm |
| resetPasswordProps | Partial<ResetPasswordFormProps> | — | Props forwarded to ResetPasswordForm |
| className | string | — | Additional CSS classes |
OAuthCallbackHandler (since 0.23)
Headless component that completes the Google sign-in round-trip. Mount it once inside <AziridProvider>. It does nothing until it sees ?ticket=… (or ?oauth_error=…) in the URL, at which point it exchanges the ticket server-side, lets the API set the session cookies, strips the params, and reloads.
import { AziridProvider, OAuthCallbackHandler } from 'azirid-react'
<AziridProvider publishableKey={pk}>
<OAuthCallbackHandler />
{/* …rest of your app */}
</AziridProvider>| Prop | Type | Default | Description |
| ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
| redirectUrl | string | <current URL cleaned> | Where to send the user after success. Default: same URL minus the OAuth params. |
| onSuccess | (user: unknown) => void | — | Called with the user object after a successful exchange. |
| onError | (error: Error) => void | — | Called when the provider returns an error or the ticket exchange fails. |
| loadingText | string | Signing you in… | Copy shown while exchanging the ticket. |
| errorText | string | generic | Fallback copy when the exchange fails and there is no specific error message. |
| silentWhenIdle | boolean | true | When true the component renders nothing while idle. Set to false to render a placeholder even when the URL has no OAuth params. |
Headless hooks
All hooks require <AziridProvider> in the tree.
useAzirid — session state
import { useAzirid } from 'azirid-react'
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>
)
}
isLoadingistrueduring session bootstrap (page load/refresh), login, and signup. Use it to show a loading state and prevent rendering authenticated UI before the session is ready. This ensures no API calls are made without a valid token.
Auth Guard pattern (Next.js)
import { useAzirid } from 'azirid-react'
function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAzirid()
if (isLoading) return <LoadingSpinner />
if (!isAuthenticated) return <RedirectToLogin />
return <>{children}</>
}useAziridOptional — non-throwing context read
Same shape as useAzirid(), but returns null when no <AziridProvider> is mounted instead of throwing. Use it in components that need to work with or without the provider (e.g. a form that optionally auto-submits to the API if a provider is present).
import { useAziridOptional } from 'azirid-react'
function OptionalSubmitter() {
const ctx = useAziridOptional()
if (!ctx) return <p>No provider — render in "offline" mode</p>
return <button onClick={() => ctx.login({ email: '…', password: '…' })}>Sign in</button>
}useLogin
import { useLogin } from 'azirid-react'
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-react'
const { signup, isLoading, error } = useSignup({
onSuccess: (data) => console.log('Registered:', data.user),
})
signup({ email: '[email protected]', password: 'secret' })useLogout
import { useLogout } from 'azirid-react'
const { logout, isLoading } = useLogout({
onSuccess: () => router.push('/login'),
})useSession
import { useSession } from 'azirid-react'
const { user, accessToken, isAuthenticated } = useSession()useMagicLink
import { useMagicLink } from 'azirid-react'
const { requestMagicLink, verifyMagicLink, isLoading } = useMagicLink()
requestMagicLink({ email: '[email protected]' })
verifyMagicLink({ token: '...' })useSocialLogin
import { useSocialLogin } from 'azirid-react'
const { loginWithProvider, isLoading } = useSocialLogin()
loginWithProvider({ provider: 'google' }) // "google" | "github"Low-level hook — assumes you already obtained a
providerAccountIdfrom the provider yourself. For the standard server-driven flow (recommended) seeuseOAuthProviders+<OAuthCallbackHandler>below.
useOAuthProviders — list enabled social providers (since 0.23)
import { useOAuthProviders } from 'azirid-react'
const { data: providers, isLoading } = useOAuthProviders()
// → [{ provider: 'google', enabled: true }]Returns the social-login providers the customer admin enabled for the current App (configured in Dashboard → Social Providers). The hook reads the publishable key from <AziridProvider> and resolves the App + environment server-side.
<LoginForm> calls this hook internally to decide which buttons to render. Use it directly only if you need a custom button layout.
useOAuthStartUrl — build a redirect URL for a custom button (since 0.23)
import { useOAuthStartUrl } from 'azirid-react'
const googleUrl = useOAuthStartUrl('google')
<button onClick={() => googleUrl && (window.location.href = googleUrl)}>
Custom Google button
</button>Builds the absolute URL the SDK should redirect to (<API>/v1/oauth/google/start?publishable_key=…). Use it when you don't want the built-in <LoginForm> button.
usePasskeys
import { usePasskeys } from 'azirid-react'
const { passkeys, registerPasskey, removePasskey, isLoading } = usePasskeys()useChangePassword
import { useChangePassword } from 'azirid-react'
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-react'
const { bootstrap, isBootstrapping } = useBootstrap()Note: The automatic bootstrap uses request deduplication — if React 18 Strict Mode (or any other scenario) triggers multiple bootstrap calls, only one HTTP request is made and all callers await the same promise. This prevents token rotation conflicts that would otherwise invalidate the session on page reload in development mode. During bootstrap,
isLoadingfromuseAzirid()istrue, so components using the Auth Guard pattern will show a loading state until the session is restored. Hooks likeusePayphoneCheckout,useSession, and<PayphoneCallback>also wait for bootstrap to complete before making authenticated requests.
useRefresh
Manually refresh the access token.
import { useRefresh } from 'azirid-react'
const { refresh } = useRefresh()useAccessToken
Returns the current access token, or null when unauthenticated. Use it to pass the token to an external API client (e.g. an openapi-fetch instance) that is NOT routed through useAccessClient(). The value re-renders on bootstrap, refresh, tenant switch, and logout.
import { useAccessToken } from 'azirid-react'
function useMyApiClient() {
const at = useAccessToken()
return useMemo(() => createExternalClient({ getAuthToken: () => at }), [at])
}useAccessTokenSync
Fires callback(current, previous) on every access-token transition (refresh, tenant switch, logout). The callback is not invoked on initial mount — only on subsequent changes. Typical use: bridge the provider to a TanStack Query client so a logout (string → null) clears cached data while silent refreshes (string → new string) do not clobber the cache.
import { useAccessTokenSync } from 'azirid-react'
useAccessTokenSync((current, previous) => {
if (previous !== null && current === null) {
queryClient.clear()
}
})useAuthStatus
Collapses isBootstrapping / isLoading / user into a single discriminant — convenient for guards and conditional UI.
import { useAuthStatus } from 'azirid-react'
const status = useAuthStatus() // 'loading' | 'authenticated' | 'unauthenticated'
if (status === 'loading') return <Spinner />
if (status === 'unauthenticated') return <LoginForm />
return <Dashboard />This is the hook that backs <AuthGuard> and <GuestGuard> under azirid-react/next/guards.
useAccessClient
Access the raw AccessClient instance for custom API calls.
import { useAccessClient } from 'azirid-react'
function CustomAction() {
const client = useAccessClient()
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-react'
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-react'
function PasswordInput() {
const { visible, toggle, type } = usePasswordToggle()
return (
<div>
<input type={type} name="password" />
<button type="button" onClick={toggle}>
{visible ? 'Hide' : 'Show'}
</button>
</div>
)
}usePasswordReset
Two-step password reset flow: request a reset email, then confirm with the token.
import { usePasswordReset } from 'azirid-react'
function PasswordResetFlow() {
const passwordReset = usePasswordReset({
onRequestSuccess: () => toast.success('Check your email'),
onConfirmSuccess: () => router.push('/login'),
onError: (err) => console.error(err),
})
// Step 1: request reset email
passwordReset.request.mutate({ email: '[email protected]' })
// Step 2: confirm with token from URL
passwordReset.confirm.mutate({ token: 'abc', newPassword: 'newPass123!' })
}Zod Schemas
The SDK exports pre-built Zod schemas with Spanish validation messages, plus factory functions for custom messages.
import {
loginSchema,
signupSchema,
changePasswordSchema,
magicLinkRequestSchema,
magicLinkVerifySchema,
socialLoginSchema,
passkeyRegisterStartSchema,
forgotPasswordSchema,
resetPasswordConfirmSchema,
} from 'azirid-react'
// Default schemas (Spanish messages)
loginSchema.parse({ email: '[email protected]', password: 'secret' })
// Factory functions for custom messages
import {
createLoginSchema,
createSignupSchema,
createForgotPasswordSchema,
createResetPasswordConfirmSchema,
} from 'azirid-react'
const customSchema = createLoginSchema({
emailRequired: 'Email is required',
emailInvalid: 'Must be a valid email',
passwordRequired: 'Password is required',
passwordMinLength: 'At least 10 characters',
})| Schema | Fields |
| ---------------------------- | --------------------------------------------------------------------- |
| loginSchema | email, password |
| signupSchema | email, password, acceptedTosVersion?, acceptedPrivacyVersion? |
| changePasswordSchema | currentPassword, newPassword |
| magicLinkRequestSchema | email |
| magicLinkVerifySchema | token |
| socialLoginSchema | provider, providerAccountId, email, emailVerified?, ... |
| passkeyRegisterStartSchema | deviceName? |
| forgotPasswordSchema | email |
| resetPasswordConfirmSchema | token, newPassword |
Billing & Payments
All billing hooks require <AziridProvider> in the tree and an authenticated user.
usePlans
Fetch all available billing plans for the current app.
import { usePlans } from 'azirid-react'
function PricingPage() {
const { data: plans, isLoading } = usePlans()
if (isLoading) return <Spinner />
return (
<ul>
{plans?.map((plan) => (
<li key={plan.id}>
{plan.name} — ${(plan.amount / 100).toFixed(2)}/{plan.interval.toLowerCase()}
</li>
))}
</ul>
)
}useSubscription
Get the current user's active subscription.
import { useSubscription } from 'azirid-react'
function SubscriptionStatus() {
const { data: sub } = useSubscription()
if (!sub) return <p>No active subscription</p>
return (
<p>
{sub.plan.name} — {sub.status}
{sub.cancelAtPeriodEnd && ' (cancels at period end)'}
</p>
)
}useCheckout
Initiate a checkout session. Auto-redirects to the payment provider on success (unless the provider is MANUAL_TRANSFER or PAYPHONE).
import { useCheckout } from 'azirid-react'
function UpgradeButton({ planId }: { planId: string }) {
const { checkout, isPending } = useCheckout({
onSuccess: (data) => console.log('Checkout created:', data),
onError: (err) => console.error(err),
})
return (
<button
disabled={isPending}
onClick={() =>
checkout({
planId,
successUrl: `${window.location.origin}/billing?success=true`,
cancelUrl: `${window.location.origin}/billing`,
})
}
>
Upgrade
</button>
)
}useInvoices
List all invoices for the authenticated user.
import { useInvoices } from 'azirid-react'
function InvoiceHistory() {
const { data: invoices } = useInvoices()
return (
<ul>
{invoices?.map((inv) => (
<li key={inv.id}>
${(inv.amount / 100).toFixed(2)} — {inv.status}
{inv.invoiceUrl && <a href={inv.invoiceUrl}>View</a>}
</li>
))}
</ul>
)
}usePaymentProviders
Fetch available payment providers for the app (e.g., Stripe, PayPal, manual transfer).
import { usePaymentProviders } from 'azirid-react'
const { data: providers } = usePaymentProviders()
// [{ provider: 'STRIPE', checkout: true, subscriptions: true }, ...]useBranches
List the branches configured for the current app (used for multi-location billing, e.g. a restaurant with several points of sale).
import { useBranches } from 'azirid-react'
const { data: branches, isLoading } = useBranches()
// [{ id, name, address?, ... }, ...]usePaymentMethods
List the payment methods configured for the current app, optionally filtered by branchId.
import { usePaymentMethods } from 'azirid-react'
// All methods for the app
const { data: methods } = usePaymentMethods()
// Only methods available at a specific branch
const { data: branchMethods } = usePaymentMethods('branch_123')useUploadTransferProof
Upload a file (image or PDF) to S3 via presigned URL for use as transfer proof.
import { useUploadTransferProof } from 'azirid-react'
const { upload, isUploading, publicUrl, error, reset } = useUploadTransferProof({
onSuccess: (result) => console.log('Uploaded:', result.publicUrl),
onError: (err) => console.error(err),
})
// Upload a file (returns { uploadUrl, publicUrl })
const result = await upload(file) // File from <input type="file">Accepted file types: PNG, JPG, WebP, PDF (max 10MB)
useSubmitTransferProof
Submit proof of a manual bank transfer payment (requires a pre-uploaded file URL).
import { useSubmitTransferProof } from 'azirid-react'
const { submit, isPending } = useSubmitTransferProof({
onSuccess: (proof) => console.log('Proof submitted:', proof.id),
})
submit({
intentId: 'intent_123',
fileUrl: 'https://storage.example.com/receipt.pdf',
amount: 29.99,
currency: 'USD',
notes: 'Bank transfer from Account #1234',
})useTransferProofs
List submitted transfer proofs and their review status.
import { useTransferProofs } from 'azirid-react'
const { data: proofs } = useTransferProofs()
// [{ id, status: 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED', ... }]usePayphoneCheckout
Hook that returns a renderable widget component and payment status. Pass an intentId and get full control over where the widget renders and how to react to the payment result.
import { usePayphoneCheckout } from 'azirid-react'
function CheckoutPage({ intentId }: { intentId: string }) {
const { PayphoneWidget, status, error } = usePayphoneCheckout({
intentId,
onSuccess: (data) => {
// data: { status: 'confirmed' | 'cancelled' | 'already_confirmed', intentId: string }
console.log('Payment result:', data.status)
},
onError: (err) => console.error(err),
})
return (
<div>
<h1>Pay</h1>
<PayphoneWidget />
{status === 'loading' && <p>Preparing checkout...</p>}
{status === 'confirming' && <p>Confirming payment...</p>}
{status === 'confirmed' && <p>Payment confirmed!</p>}
{status === 'cancelled' && <p>Payment was cancelled</p>}
{status === 'error' && <p>Error: {error?.message}</p>}
</div>
)
}| Option | Type | Description |
|--------|------|-------------|
| intentId | string | Required. Payment intent ID |
| onSuccess | (data) => void | Called after payment confirmation |
| onError | (error) => void | Called on any error |
| Return | Type | Description |
|--------|------|-------------|
| PayphoneWidget | () => ReactElement \| null | Component to render the Payphone widget |
| status | PayphoneCheckoutStatus | 'idle' \| 'loading' \| 'ready' \| 'confirming' \| 'confirmed' \| 'cancelled' \| 'error' |
| intentId | string | The intent ID passed in |
| error | Error \| null | Error details if status is 'error' |
The hook handles both phases automatically:
- Widget phase: Calls checkout API, loads Payphone SDK, widget renders via
<PayphoneWidget />. - Confirmation phase: After Payphone redirects back to your configured Response URL (set in Payphone Developer dashboard), detects
?id=X&clientTransactionId=Yparams and confirms the payment.
Important: The redirect URL after payment is configured in the Payphone Developer dashboard (not passed via code). Make sure your Response URL points to the page where
usePayphoneCheckoutor<PayphoneCallback>is rendered.
Session safety: The confirmation request waits for the session bootstrap to complete before firing. This prevents 401 errors when Payphone redirects back and the page reloads — the SDK restores the session first, then confirms the payment.
usePayButton
Hook that returns renderable components and state for a complete payment flow. Supports all providers (Stripe, PayPal, Payphone, Manual Transfer, Nuvei). You control the layout.
import { usePayButton } from 'azirid-react'
function CheckoutPage({ intentId }: { intentId: string }) {
const {
providers, checkout, ProviderSelector,
TransferDetails, PayphoneWidget, status, error,
} = usePayButton({
intentId,
onSuccess: (data) => console.log('Paid!', data),
onError: (err) => console.error(err),
})
return (
<div>
{/* Option A: built-in provider selector */}
<ProviderSelector />
{/* Option B: custom UI with providers array */}
{providers.map(p => (
<button key={p.provider} onClick={() => checkout(p.provider)}>
{p.provider}
</button>
))}
{/* Renders automatically based on selected provider */}
<PayphoneWidget />
<TransferDetails />
{status === 'processing' && <p>Processing...</p>}
{status === 'redirecting' && <p>Redirecting to payment...</p>}
{status === 'error' && <p>Error: {error?.message}</p>}
</div>
)
}| Option | Type | Description |
|--------|------|-------------|
| intentId | string | Payment intent ID |
| planId | string | Plan ID (alternative to intentId) |
| onSuccess | (data) => void | Called after successful payment |
| onError | (error) => void | Called on any error |
| Return | Type | Description |
|--------|------|-------------|
| providers | AvailableProvider[] | Enabled payment providers |
| checkout | (provider) => void | Trigger checkout for a specific provider |
| ProviderSelector | Component | Built-in provider selection list |
| TransferDetails | Component | Bank transfer details (after MANUAL_TRANSFER checkout) |
| PayphoneWidget | Component | Payphone widget (after PAYPHONE checkout) |
| status | PayButtonStatus | 'idle' \| 'loading_providers' \| 'ready' \| 'processing' \| 'redirecting' \| 'success' \| 'error' |
| selectedProvider | PaymentProviderType \| null | Currently selected provider |
| checkoutData | CheckoutResponse \| null | Full checkout response |
| isPending | boolean | Checkout API call in progress |
| error | Error \| null | Error details |
useTransferPayment
Hook for manual bank transfer payments. Pass an intentId and get a component with bank details + functions to upload and submit proof of payment.
import { useTransferPayment } from 'azirid-react'
function TransferPage({ intentId }: { intentId: string }) {
const {
TransferDetails, uploadAndSubmitProof, status, error,
} = useTransferPayment({
intentId,
onProofSubmitted: (proof) => console.log('Proof sent:', proof.id),
onError: (err) => console.error(err),
})
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) uploadAndSubmitProof(file, 'Transfer ref #123')
}
return (
<div>
<TransferDetails />
{status === 'ready' && <input type="file" accept="image/*,.pdf" onChange={handleFileChange} />}
{status === 'uploading' && <p>Uploading...</p>}
{status === 'submitting' && <p>Submitting...</p>}
{status === 'submitted' && <p>Proof submitted!</p>}
{status === 'error' && <p>Error: {error?.message}</p>}
</div>
)
}| Option | Type | Description |
|--------|------|-------------|
| intentId | string | Required. Payment intent ID |
| onSuccess | (data) => void | Called after checkout completes |
| onProofSubmitted | (proof) => void | Called after proof is submitted |
| onError | (error) => void | Called on any error |
| Return | Type | Description |
|--------|------|-------------|
| TransferDetails | Component | Amount, bank details, and transfer instructions |
| uploadAndSubmitProof | (file: File, notes?: string) => Promise<void> | Upload file to S3 and submit proof in one step |
| submitProof | (data) => void | Submit transfer proof with pre-uploaded file URL |
| status | TransferPaymentStatus | 'idle' \| 'loading' \| 'ready' \| 'uploading' \| 'submitting' \| 'submitted' \| 'error' |
| checkoutData | CheckoutResponse \| null | Full checkout response |
| isUploading | boolean | File upload in progress |
| isSubmitting | boolean | Proof submission in progress |
| error | Error \| null | Error details |
usePayphoneConfirm
Confirm a Payphone payment callback. Used on the Payphone return URL page.
import { usePayphoneConfirm } from 'azirid-react'
const confirm = usePayphoneConfirm({
onSuccess: (data) => console.log('Payment confirmed:', data.status),
})
confirm.mutate({ id: 12345, clientTransactionId: 'txn_abc' })Billing Components
PricingTable
Drop-in pricing grid with checkout flow integrated.
import { PricingTable } from 'azirid-react'
;<PricingTable
successUrl={`${window.location.origin}/billing?success=true`}
cancelUrl={`${window.location.origin}/billing`}
columns={3}
onPlanSelect={(plan) => console.log('Selected:', plan.name)}
/>| Prop | Type | Default | Description |
| -------------- | ---------------- | ------- | --------------------------------------------------- |
| successUrl | string | — | Required. Redirect URL after successful payment |
| cancelUrl | string | — | Required. Redirect URL on cancel |
| columns | number | 3 | Number of columns in the grid |
| onPlanSelect | (plan) => void | — | Called when a plan is selected |
| className | string | — | Additional CSS classes |
PayButton
Flexible payment button with provider selection modal. Supports both plans and payment intents.
import { PayButton } from 'azirid-react'
;<PayButton
planId="plan_123"
successUrl="/billing?success=true"
cancelUrl="/billing"
onSuccess={(data) => console.log('Payment success:', data)}
>
Subscribe Now
</PayButton>| Prop | Type | Default | Description |
| ------------ | ----------------- | ------- | --------------------------------------------- |
| planId | string | — | Plan to purchase (use planId or intentId) |
| intentId | string | — | Payment intent ID (alternative to planId) |
| successUrl | string | — | Required. Redirect URL after success |
| cancelUrl | string | — | Required. Redirect URL on cancel |
| onSuccess | (data) => void | — | Called on successful checkout. For MANUAL_TRANSFER and PAYPHONE, deferred until the user closes the modal |
| onError | (error) => void | — | Called on error |
| onProviderSelect | (provider: string) => void | — | Called when a provider is selected |
| onProofSubmitted | (proof) => void | — | Called when transfer proof is submitted (MANUAL_TRANSFER only) |
| children | ReactNode | — | Button label |
| disabled | boolean | — | Disable the button |
CheckoutButton
Simple checkout button for a specific plan.
import { CheckoutButton } from 'azirid-react'
;<CheckoutButton planId="plan_123" successUrl="/billing?success=true" cancelUrl="/billing">
Subscribe
</CheckoutButton>SubscriptionBadge
Color-coded badge showing the current subscription status.
import { SubscriptionBadge } from 'azirid-react'
;<SubscriptionBadge />
// Renders: "Pro · Active" (green), "Free · Trialing" (blue), etc.Status colors: ACTIVE (green), TRIALING (blue), PAST_DUE (yellow), CANCELED/UNPAID (red), INCOMPLETE (gray).
InvoiceList
Table of invoices with status badges and download links.
import { InvoiceList } from 'azirid-react'
;<InvoiceList />PayphoneCallback
Page component for handling Payphone payment callbacks. Reads id and clientTransactionId from URL query params automatically. Deploy this page at the Response URL configured in your Payphone Developer dashboard. The component waits for the session bootstrap to complete before confirming, preventing 401 errors on page reload.
// app/payphone/callback/page.tsx
import { PayphoneCallback } from 'azirid-react'
export default function PayphoneCallbackPage() {
return (
<PayphoneCallback
onSuccess={(data) => console.log('Confirmed:', data.status)}
onError={(err) => console.error(err)}
/>
)
}Multi-tenant
Every authenticated user always has an active tenantId and tenantRole in the JWT. On signup, if no tenantId is provided, a personal tenant is automatically created and the user becomes OWNER.
Tenant switching via useAzirid()
import { useAzirid } from 'azirid-react'
function TenantSwitcher({ tenantId }: { tenantId: string }) {
const { user, switchTenant } = useAzirid()
return (
<div>
<p>
Active tenant: {user?.tenantId} ({user?.tenantRole})
</p>
<button onClick={() => switchTenant(tenantId)}>Switch</button>
</div>
)
}switchTenant(tenantId) calls the refresh endpoint with the new tenantId, updates the access token in memory, and invalidates all queries — no re-login required.
useTenants
List all tenants the authenticated user belongs to.
import { useTenants } from 'azirid-react'
function TenantList() {
const { data: tenants, isLoading } = useTenants()
if (isLoading) return <Spinner />
return (
<ul>
{tenants?.map((t) => (
<li key={t.tenantId}>
{t.name} — {t.role}
</li>
))}
</ul>
)
}useTenantMembers
List all members of a specific tenant. The authenticated user must be a member.
import { useTenantMembers } from 'azirid-react'
function MemberList({ tenantId }: { tenantId: string }) {
const { data: members } = useTenantMembers(tenantId)
return (
<ul>
{members?.map((m) => (
<li key={m.id}>
{m.user.email} — {m.role}
</li>
))}
</ul>
)
}useSwitchTenant
Headless hook for tenant switching (same as switchTenant from useAzirid(), but usable outside the main context hook).
import { useSwitchTenant } from 'azirid-react'
const { switchTenant } = useSwitchTenant()
await switchTenant('tenant-id-here')Referrals
All referral hooks require <AziridProvider> in the tree and an authenticated user.
useReferral
Fetch the current user's referral info and copy referral link to clipboard.
import { useReferral } from 'azirid-react'
function ReferralSection() {
const { data, copyToClipboard } = useReferral()
if (!data) return null
return (
<div>
<p>Your referral code: {data.referralCode}</p>
<input readOnly value={data.referralUrl} />
<button onClick={copyToClipboard}>Copy Link</button>
<p>
{data.completedReferrals} completed / {data.totalReferred} total
</p>
</div>
)
}useReferralStats
Fetch detailed referral history with rewards.
import { useReferralStats } from 'azirid-react'
function ReferralHistory() {
const { data } = useReferralStats()
return (
<ul>
{data?.referrals.map((ref) => (
<li key={ref.id}>
{ref.referredEmail} — {ref.status}
{ref.rewardAmount && ` ($${ref.rewardAmount})`}
</li>
))}
</ul>
)
}Referral Components
ReferralCard
Card displaying the referral link with copy button and stats.
import { ReferralCard } from 'azirid-react'
;<ReferralCard
title="Refer a Friend"
description="Share your link and earn rewards for each signup."
/>| Prop | Type | Default | Description |
| ------------- | -------- | ------------------ | ---------------------- |
| title | string | "Refer a Friend" | Card title |
| description | string | — | Card description |
| className | string | — | Additional CSS classes |
ReferralStats
Table showing referral history with status and reward badges.
import { ReferralStats } from 'azirid-react'
;<ReferralStats />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-react'
;<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-react'
function CustomForm() {
const msg = useMessages() // resolved messages for current locale
return <label>{msg.login.emailLabel}</label>
}i18n exports
The SDK exports the raw dictionaries and the resolver function for advanced use cases:
import { resolveMessages, es, en } from 'azirid-react'
// Get the full Spanish dictionary
console.log(es.login.title) // "Iniciar sesión"
// Resolve with overrides
const messages = resolveMessages('en', {
login: { title: 'Welcome back!' },
})Locale-aware Zod schemas
import { createLoginSchema, createSignupSchema } from 'azirid-react'
// Pass custom validation messages
const schema = createLoginSchema({
emailRequired: 'Email is required',
emailInvalid: 'Must be a valid email',
passwordRequired: 'Password is required',
passwordMinLength: 'At least 10 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 (overrides
--aa-primaryvia inline style on the shadow host — see Styling & theming for per-token overrides like--aa-radius,--aa-foreground, etc.) - 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-react'
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 badge renders automatically below each built-in form component (LoginForm, SignupForm, etc.). It's hidden when branding.removeBranding is true (configurable in the dashboard). No extra code needed.
createAccessClient
Under the hood AziridProvider creates an AccessClient via createAccessClient. You can also create a client directly to make raw API calls.
import { createAccessClient, AUTH_BASE_PATH } from 'azirid-react'
import type { AccessClientConfig } from 'azirid-react'
const client = createAccessClient(
{
baseUrl: 'https://api.azirid.com',
basePath: AUTH_BASE_PATH, // '/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 |
| 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. Runs once (safe in React 18 Strict Mode) |
| refreshInterval | number | 240000 | Token refresh interval in ms. 0 to disable. Tokens also refresh reactively on any 401 response, so a higher interval is safe. |
| onAuthStateChange| () => void | — | Called after login, signup, or logout |
| onLoginSuccess | (data) => void | — | Called after successful login |
| onSignupSuccess | (data) => void | — | Called after successful signup |
| onLogoutSuccess | () => void | — | Called after logout |
| onSessionExpired | () => void | — | Called when the refresh token is definitively rejected (401/403). Use this to redirect to login. |
| 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) |
Device Tracking
The SDK automatically generates a persistent device ID (UUIDv4) and stores it in localStorage under the key __az_device_id. This ID is sent as an X-Device-Id header on every auth request (login, signup, refresh, bootstrap).
How it works:
- On first use,
crypto.randomUUID()generates a unique ID per browser profile - The ID persists across tabs, page reloads, and browser restarts
- Different browsers or incognito windows get different device IDs
- The backend uses this to deduplicate sessions per device: re-logging from the same browser replaces the previous session instead of creating duplicates
Where session material lives, by mode:
| Key | Storage | Mode | Purpose |
| ---------------- | ----------------- | ------------- | -------------------------------------------------------------------- |
| __az_device_id | localStorage | both | Persistent device identity — survives browser restarts |
| __session | HttpOnly cookie | both | Access token — server-owned (Set-Cookie on every auth response) |
| _asid | HttpOnly cookie | same-origin | Refresh token — server-owned, never touched by JS |
| _axc | cookie | same-origin | CSRF token — paired with _asid |
| __az_rt | sessionStorage | cross-origin | Refresh token mirror — tab-scoped, cleared on tab close |
| __az_xc | sessionStorage | cross-origin | CSRF token mirror — tab-scoped |
The SDK picks the right lane automatically at mount (see Cross-origin session handling): same-origin deployments rely entirely on HttpOnly cookies, cross-origin deployments mirror the refresh/CSRF tokens into sessionStorage so the session survives full-page navigations.
Legacy keys (
__azrt/__azxcinlocalStorage) were removed in 0.18. The SDK no longer reads or writes them. If you are upgrading from 0.17 or earlier, users will re-authenticate once after the first deploy.In SSR or environments where
localStorage/sessionStorageis unavailable, storage operations are silently skipped.
Impersonation Handoff
The Azirid dashboard lets admins click "Sign in as user" to impersonate any end user in a customer app. This works via a short-lived handoff code that your app exchanges for real session tokens.
How it works:
- Admin clicks "Sign in as user" in the Azirid dashboard
- Dashboard generates a one-time handoff code (valid for 5 minutes) and redirects to your app at
/auth/handoff?code=<code>&api=<apiUrl> - Your app renders
<HandoffCallback>, which exchanges the code for session tokens directly with the API - Tokens are stored in
localStorage+__sessioncookie, then the component redirects toredirectUrlvia hard navigation soAziridProviderre-bootstraps with the new session
<HandoffCallback>is fully standalone — it does NOT depend onAziridProvider. It works even when the provider wraps the entire layout and its bootstrap would redirect to/login. No special route groups or layout changes needed.
Setup — create the handoff page:
// app/auth/handoff/page.tsx
'use client'
import { HandoffCallback } from 'azirid-react'
export default function HandoffPage() {
return (
<HandoffCallback
redirectUrl="/dashboard"
onError={() => window.location.href = '/login'}
/>
)
}Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| redirectUrl | string | "/" | URL to redirect after successful handoff (hard navigation) |
| onSuccess | (user: unknown) => void | — | Called before redirect for optional side effects (analytics, etc.) |
| onError | (error: Error) => void | — | Called on failure (expired/invalid code); redirect to login |
| loadingText | string | "Signing you in..." | Text shown while the exchange is in progress |
| errorText | string | "Failed to complete sign-in. The link may have expired." | Fallback error text |
You can also call client.exchangeHandoff(code, apiUrl) directly if you prefer a headless approach:
const { user } = await client.exchangeHandoff(code, 'https://api.azirid.com/v1')Next.js Integration
azirid-react supports Next.js 14, 15, and 16+ with full compatibility for each version's API conventions.
Server Actions & onAuthStateChange
If you use server actions or server components that read the session token (via createServerAccess), you must pass onAuthStateChange to keep server-side cookies in sync:
'use client'
import { useRouter } from 'next/navigation'
import { AziridProvider } from 'azirid-react'
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<AziridProvider
publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}
onAuthStateChange={() => router.refresh()}
>
{children}
</AziridProvider>
)
}Why? After login/signup/logout, azirid-react updates the __session cookie in the browser. But Next.js server actions keep using the old cookies until router.refresh() forces a new server request. Without this, server actions may return 401 after login.
Not using server actions? (e.g. pure client-side SPA with Vite) — you can skip
onAuthStateChange.
Route Protection — <AuthGuard> and <GuestGuard>
azirid-react calls api.azirid.com cross-origin from your app, so the auth
cookies set by the API live on the Azirid domain — they are not visible
to your app's domain. That makes server-side cookie-based gating impossible
for SaaS consumers, so route protection happens client-side after the
provider bootstraps.
Setup
Group your protected routes under a layout that wraps them in <AuthGuard>,
and your auth pages under a layout that wraps them in <GuestGuard>:
// app/(protected)/layout.tsx
import { AuthGuard } from 'azirid-react/next/guards'
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
return <AuthGuard>{children}</AuthGuard>
}// app/(auth)/layout.tsx
import { GuestGuard } from 'azirid-react/next/guards'
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <GuestGuard>{children}</GuestGuard>
}Then move your routes into those groups:
app/
(protected)/
dashboard/page.tsx
admin/page.tsx
(auth)/
login/page.tsx
register/page.tsx
forgot-password/page.tsxHow it works
<AuthGuard> — for pages that require authentication:
- Renders
fallback(default:null) while the provider bootstraps. - When bootstrap finishes and the user is not logged in,
router.replace()to/login?redirect=<original-pathname>. - When the user is authenticated, renders
children.
<GuestGuard> — for login/register/etc. (logged-OUT only):
- Renders
fallbackwhile the provider bootstraps. - When the user is authenticated, redirects to
?redirect=<path>(if present and starts with/) or todashboardUrl(default:/dashboard). - When the user is logged out, renders
children.
Props
<AuthGuard>:
| Prop | Type | Default | Description |
| ------------------- | ----------- | ----------- | ------------------------------------------------------------- |
| loginUrl | string | '/login' | Redirect target for unauthenticated users |
| preserveRedirect | boolean | true | Append ?redirect=<pathname> so login can send the user back |
| fallback | ReactNode | null | Rendered while loading or during the redirect |
<GuestGuard>:
| Prop | Type | Default | Description |
| ------------------- | ----------- | -------------- | ----------------------------------------------------------------- |
| dashboardUrl | string | '/dashboard' | Redirect target for authenticated users |
| honorRedirectParam| boolean | true | When true, redirects to ?redirect=<path> from the URL if vali
