Typed Client & TanStack Query
useResource, createAuthkitClient, AuthkitClientProvider, query/mutation hooks, query keys, and error handling for the admin and account APIs.
@adonis-agora/authkit-react ships a typed HTTP client and a full set of
TanStack Query hooks for both the admin console API and the account self-service API.
This lets your React UI fetch and mutate data with full TypeScript inference — no hand-written
fetch calls, no manual cache invalidation boilerplate.
The login and consent flows remain server-side postback for security. The client/hooks layer is for management screens (admin UIs, account pages) that need rich, reactive data.
┌────────────────────────────────┐
│ createAuthkitClient(opts?) │ ← low-level typed fetch wrapper
│ client.admin.* │
│ client.account.* │
└──────────────┬─────────────────┘
│ wrapped by
┌──────────────▼─────────────────┐
│ TanStack Query hooks │ ← useUsersQueryOptions(), useMutation(…)
│ authkitKeys.* │ ← structured cache keys
└────────────────────────────────┘Install
@tanstack/react-query is a peer dependency. Install it alongside the package:
pnpm add @adonis-agora/authkit-react @tanstack/react-queryBuilding your own hook with useResource
Before the typed client, there is a smaller escape hatch. Every headless hook in
@adonis-agora/authkit-react — useProfile, useSessions,
useOrganizations, and the rest — is useResource plus a couple of actions built on
jsonRequest. Both are exported, so a screen that needs an endpoint AuthKit knows nothing
about does not have to switch fetching styles (or pull in React Query for one list).
function useResource<T>(url: string, csrfToken?: string): {
data: T | null
loading: boolean
error: Error | null
refetch: () => Promise<void>
}
function jsonRequest<T>(url: string, init?: RequestInit & { csrfToken?: string }): Promise<T>jsonRequest is the one-shot primitive: it always sends credentials: 'same-origin' and
Accept: application/json, adds Content-Type: application/json when there is a body and
X-CSRF-TOKEN when you pass csrfToken, throws an Error on any non-2xx response (using the
server's message field when the error body has one), and resolves to null on an empty
204. useResource wraps it in a GET-on-mount effect: it only fetches inside the effect, so
it is SSR-safe, and it refetches whenever the URL or the token changes.
Building your own hook is then mostly naming things:
import { useCallback } from 'react'
import { jsonRequest, useResource, useAuthkitConfig } from '@adonis-agora/authkit-react'
interface ApiKey {
id: string
label: string
createdAt: string
}
export function useApiKeys() {
const { csrfToken } = useAuthkitConfig()
const { data, loading, error, refetch } = useResource<ApiKey[]>('/api/keys', csrfToken)
const revoke = useCallback(
async (id: string) => {
await jsonRequest(`/api/keys/${encodeURIComponent(id)}`, {
method: 'DELETE',
csrfToken,
})
await refetch()
},
[csrfToken, refetch],
)
return { data, loading, error, actions: { refetch, revoke } }
}Two details worth copying from the built-in hooks: read csrfToken from
useAuthkitConfig() instead of threading it through props, and return mutations under an
actions object so the hook keeps the { data, loading, error, actions } shape the rest of
the kit uses. When a mutation needs to be reflected in the UI, await refetch() after it —
there is no cache to invalidate at this layer, which is exactly the trade-off you accept for
not needing a QueryClient.
Reach for the typed client and the TanStack hooks below when a screen has many interlocking
queries, needs shared caching across components, or wants optimistic updates. Reach for
useResource when a screen needs one list and a refresh.
Provider setup
Wrap your app (or the subtree that needs data access) once:
import { QueryClientProvider } from '@tanstack/react-query'
import {
createAuthkitQueryClient,
AuthkitClientProvider,
} from '@adonis-agora/authkit-react'
const queryClient = createAuthkitQueryClient()
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AuthkitClientProvider>
{children}
</AuthkitClientProvider>
</QueryClientProvider>
)
}createAuthkitQueryClient() creates a QueryClient pre-tuned for AuthKit:
| Default | Value |
|---|---|
staleTime | 30 s |
gcTime | 5 min |
retry | 1 |
refetchOnWindowFocus | false |
You can bring your own QueryClient — the helper is purely optional.
AuthkitClientProvider reads window.__AUTHKIT__ (injected by the admin shell) to
configure the base URLs and CSRF token automatically. In SSR or tests, pass an explicit
client:
import { createAuthkitClient, AuthkitClientProvider } from '@adonis-agora/authkit-react'
const client = createAuthkitClient({ baseUrl: '/admin/api', csrfToken: 'test-token' })
<AuthkitClientProvider client={client}>
<App />
</AuthkitClientProvider>The typed client
createAuthkitClient(opts?) returns a typed wrapper around fetch. It uses
window.__AUTHKIT__ for URLs and CSRF by default.
import { createAuthkitClient } from '@adonis-agora/authkit-react'
// Browser — reads window.__AUTHKIT__ automatically
const client = createAuthkitClient()
// Explicit (SSR, tests, custom topologies)
const client = createAuthkitClient({
baseUrl: '/admin/api', // default: window.__AUTHKIT__.endpoints.api
accountBaseUrl: '/account/api', // default: '/account/api'
csrfToken: 'my-token',
fetch: customFetch, // optional — inject a mock in tests
})Admin surface — client.admin.*
| Method | HTTP | Notes |
|---|---|---|
admin.overview() | GET /overview | Dashboard metrics |
admin.users.list(params?) | GET /users | { search?, page?, size? } → { meta, data } |
admin.users.get(id) | GET /users/:id | Single user |
admin.users.create(data) | POST /users | Creates or invites |
admin.users.update(id, data) | PATCH /users/:id | Partial update |
admin.users.disable(id) | POST /users/:id/disable | Blocks login |
admin.users.enable(id) | POST /users/:id/enable | Re-enables account |
admin.users.resetPassword(id) | POST /users/:id/reset-password | Sends email |
admin.users.remove(id) | DELETE /users/:id | Cascade delete |
admin.users.getSessions(id) | GET /users/:id/sessions | Active sessions |
admin.users.revokeSessions(id) | POST /users/:id/revoke-sessions | Revoke all |
admin.sessions.list(accountId?) | GET /sessions | All active sessions |
admin.sessions.revokeAll(accountId?) | POST /sessions/revoke-all | — |
admin.clients.list() | GET /clients | — |
admin.clients.get(id) | GET /clients/:id | — |
admin.clients.create(data?) | POST /clients | — |
admin.clients.update(id, data?) | PATCH /clients/:id | — |
admin.clients.remove(id) | DELETE /clients/:id | — |
admin.clients.regenerateSecret(id) | POST /clients/:id/regenerate-secret | Shown once |
admin.roles.list() | GET /roles | — |
admin.roles.create(data) | POST /roles | — |
admin.roles.update(name, data) | PATCH /roles/:name | — |
admin.roles.remove(name) | DELETE /roles/:name | — |
admin.orgs.list() | GET /orgs | — |
admin.orgs.get(id) | GET /orgs/:id | — |
admin.orgs.create(data) | POST /orgs | — |
admin.orgs.update(id, data) | PATCH /orgs/:id | — |
admin.orgs.remove(id) | DELETE /orgs/:id | — |
admin.orgs.addMember(orgId, { accountId, role }) | POST /orgs/:id/members | Adds an existing account to the org |
admin.orgs.removeMember(orgId, accountId) | DELETE /orgs/:id/members/:accountId | — |
admin.orgs.updateMemberRole(orgId, accountId, role) | PATCH /orgs/:id/members/:accountId | Sends { role } |
admin.orgs.createInvitation(orgId, { email, role }) | POST /orgs/:id/invitations | For someone who has no account yet |
admin.orgs.revokeInvitation(orgId, invitationId) | DELETE /orgs/:id/invitations/:invitationId | — |
admin.audit.list(params?) | GET /audit | { type?, page?, size?, subject? } → { meta, data } |
admin.settings.list(orgId?) | GET /settings | orgId scopes to one org; omitted = global |
admin.settings.set(key, value, orgId?) | PUT /settings/:key | Same scoping |
admin.settings.remove(key, orgId?) | DELETE /settings/:key | Resets to the library default |
admin.impersonation.get(userId) | GET /impersonation/:userId | Panel data |
admin.keys.status() | GET /keys | Signing-key age, policy, next-rotation ETA |
admin.keys.rotate(input?) | POST /keys/rotate | { retire?, keep? } |
All methods under admin.* hit {adminBase}/api/* — the prefix is derived from
window.__AUTHKIT__.endpoints.api or opts.baseUrl.
Account surface — client.account.*
| Method | HTTP | Notes |
|---|---|---|
account.me() | GET /account/api/me | Profile + capability flags + sudo state |
account.security() | GET /account/api/security | Sessions, MFA state, pending email |
account.updateProfile(data) | PATCH /account/api/profile | Name / avatar URL |
account.changePassword(data) | POST /account/api/password | Requires sudo + current pw |
account.emailChange(data) | POST /account/api/email-change | Requires sudo |
account.cancelEmailChange() | POST /account/api/email-change/cancel | — |
account.sessions.list() | GET /account/api/sessions | — |
account.sessions.revoke(id) | DELETE /account/api/sessions/:id | — |
account.sessions.revokeOthers() | POST /account/api/sessions/revoke-others | — |
account.sessions.revokeAll() | POST /account/api/sessions/revoke-all | Sign out of all devices, including the current one |
account.apps.list() | GET /account/api/apps | Authorized OAuth grants |
account.apps.revoke(clientId) | DELETE /account/api/apps/:clientId | — |
account.mfa() | GET /account/api/mfa | TOTP + passkeys + recovery status |
account.passkeys.list() | GET /account/api/passkeys | — |
account.passkeys.remove(id) | DELETE /account/api/passkeys/:id | Requires sudo |
account.tokens.list() | GET /account/api/tokens | Personal Access Tokens |
account.tokens.create(data?) | POST /account/api/tokens | Requires sudo; secret returned once |
account.tokens.remove(id) | DELETE /account/api/tokens/:id | Requires sudo |
account.orgs.list() | GET /account/api/orgs | Orgs the signed-in user belongs to |
account.orgs.get(id) | GET /account/api/orgs/:id | Org detail; requires membership |
account.orgs.invitations() | GET /account/api/orgs/invitations | Pending invitations |
All account.* methods use session authentication (the accountGuard cookie). Mutating
operations (PATCH, POST, DELETE) send X-CSRF-TOKEN automatically.
AuthkitClientError
Every non-2xx response throws AuthkitClientError:
import { AuthkitClientError } from '@adonis-agora/authkit-react'
try {
await client.admin.users.get('u_missing')
} catch (err) {
if (err instanceof AuthkitClientError) {
console.log(err.status) // 404
console.log(err.code) // 'not_found' (from the error envelope)
console.log(err.message) // human-readable message
console.log(err.body) // raw JSON response
console.log(err.isUnauthorized) // true when status === 401
}
}Detect session expiry with isUnauthorized and redirect to login:
catch (err) {
if (err instanceof AuthkitClientError && err.isUnauthorized) {
window.location.href = '/auth/login'
}
}Query hooks
All query hooks return UseQueryOptions — pass them directly to useQuery:
const { data } = useQuery(useUsersQueryOptions({ search: 'alice', page: 1 }))
// data.data — AdminUser[]
// data.meta — { page, size, total }Paginated listings (users, audit) answer the { meta, data } envelope Lucid's own
.paginate() uses, which is the shape every @adonis-agora/* package returns. The request
side is { page, size }, matching @adonis-agora/filter.
Admin query hooks
| Hook | Query key | Notes |
|---|---|---|
useOverviewQueryOptions() | authkitKeys.admin.overview() | Dashboard metrics |
useUsersQueryOptions(params?) | authkitKeys.admin.users(params) | { search?, page?, size? } → { meta, data } |
useUserQueryOptions(id) | authkitKeys.admin.user(id) | Disabled when id is empty |
useUserSessionsQueryOptions(id) | authkitKeys.admin.userSessions(id) | — |
useSessionsQueryOptions(accountId?) | authkitKeys.admin.sessions(accountId) | — |
useClientsQueryOptions() | authkitKeys.admin.clients() | — |
useClientQueryOptions(id) | authkitKeys.admin.client(id) | — |
useRolesQueryOptions() | authkitKeys.admin.roles() | — |
useOrgsQueryOptions() | authkitKeys.admin.orgs() | — |
useOrgQueryOptions(id) | authkitKeys.admin.org(id) | — |
useAuditQueryOptions(params?) | authkitKeys.admin.audit(params) | { type?, page?, size?, subject? } → { meta, data } |
useSettingsQueryOptions(orgId?) | authkitKeys.admin.settings(orgId) | orgId scopes to one org; omitted/null = global |
useImpersonationQueryOptions(userId) | authkitKeys.admin.impersonation(userId) | — |
useKeysQueryOptions() | authkitKeys.admin.keys() | Signing-key status |
Account query hooks
| Hook | Query key | Notes |
|---|---|---|
useMeQueryOptions() | authkitKeys.account.me() | Profile + capability flags |
useSecurityQueryOptions() | authkitKeys.account.security() | Security overview |
useAccountSessionsQueryOptions() | authkitKeys.account.sessions() | — |
useAppsQueryOptions() | authkitKeys.account.apps() | Authorized apps |
useMfaQueryOptions() | authkitKeys.account.mfa() | MFA status |
usePasskeysQueryOptions() | authkitKeys.account.passkeys() | — |
useTokensQueryOptions() | authkitKeys.account.tokens() | PATs |
useAccountOrgsQueryOptions() | authkitKeys.account.orgs() | — |
useAccountOrgQueryOptions(id) | authkitKeys.account.org(id) | — |
useAccountOrgInvitationsQueryOptions() | authkitKeys.account.orgInvitations() | — |
Mutation hooks
Mutation hooks also return options objects — pass them to useMutation:
const m = useMutation(useCreateUserMutationOptions())Put onSuccess/onError logic and query invalidation in the calling component's handler,
not inside the hook options. This keeps hooks as pure configuration and gives each component
full control.
Admin mutation hooks
| Hook | Argument | Notes |
|---|---|---|
useCreateUserMutationOptions() | CreateUserInput | — |
useUpdateUserMutationOptions(id) | UpdateUserInput | id bound at hook call |
useDisableUserMutationOptions(id) | void | — |
useEnableUserMutationOptions(id) | void | — |
useResetPasswordMutationOptions(id) | void | Sends email |
useDeleteUserMutationOptions(id) | void | Cascade delete |
useRevokeUserSessionsMutationOptions(id) | void | — |
useRevokeAllSessionsMutationOptions(accountId?) | void | — |
useCreateClientMutationOptions() | CreateClientInput? | — |
useUpdateClientMutationOptions(id) | UpdateClientInput? | — |
useDeleteClientMutationOptions(id) | void | — |
useRegenerateClientSecretMutationOptions(id) | void | Secret returned once |
useCreateRoleMutationOptions() | CreateRoleInput | — |
useUpdateRoleMutationOptions(name) | UpdateRoleInput | — |
useDeleteRoleMutationOptions(name) | void | — |
useCreateOrgMutationOptions() | CreateOrgInput | — |
useUpdateOrgMutationOptions(id) | UpdateOrgInput | — |
useDeleteOrgMutationOptions(id) | void | — |
useAddOrgMemberMutationOptions(orgId) | { accountId: string; role: string } | Adds an existing account |
useRemoveOrgMemberMutationOptions(orgId, accountId) | void | Both ids bound at hook call |
useUpdateOrgMemberRoleMutationOptions(orgId, accountId) | string (role) | — |
useCreateOrgInvitationMutationOptions(orgId) | { email: string; role: string } | Returns the created invitation |
useRevokeOrgInvitationMutationOptions(orgId, invitationId) | void | — |
useSetSettingMutationOptions(orgId?) | { key: string; value: unknown } | orgId writes in the org scope |
useRemoveSettingMutationOptions(orgId?) | string (key) | Resets to lib default |
useRotateKeysMutationOptions() | KeysRotateInput? | { retire?, keep? } |
Account mutation hooks
| Hook | Argument | Notes |
|---|---|---|
useUpdateProfileMutationOptions() | UpdateProfileInput | — |
useChangePasswordMutationOptions() | ChangePasswordInput | Requires sudo + current password |
useEmailChangeMutationOptions() | RequestEmailChangeInput | Requires sudo |
useCancelEmailChangeMutationOptions() | void | — |
useRevokeSessionMutationOptions() | string (session id) | — |
useRevokeOtherSessionsMutationOptions() | void | — |
useAccountRevokeAllSessionsMutationOptions() | void | Sign out of all devices, including the current session |
useRevokeAppMutationOptions() | string (clientId) | — |
useRemovePasskeyMutationOptions() | string (passkey id) | Requires sudo |
useCreateTokenMutationOptions() | CreateTokenInput? | Requires sudo; secret returned once |
useRevokeTokenMutationOptions() | string (token id) | Requires sudo |
Full example — users page
The pattern: call the hook to get options, pass to useQuery/useMutation, put all
success/error/invalidation logic in the handler.
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
authkitKeys,
useUsersQueryOptions,
useCreateUserMutationOptions,
useDisableUserMutationOptions,
useDeleteUserMutationOptions,
AuthkitClientError,
} from '@adonis-agora/authkit-react'
export default function UsersPage() {
const queryClient = useQueryClient()
const [search, setSearch] = useState('')
const [disablingId, setDisablingId] = useState<string | null>(null)
// ── Queries ──────────────────────────────────────────────────────────────
const { data, isLoading } = useQuery(useUsersQueryOptions({ search }))
// ── Mutations ─────────────────────────────────────────────────────────────
const createMutation = useMutation(useCreateUserMutationOptions())
const disableMutation = useMutation(useDisableUserMutationOptions(disablingId ?? ''))
const deleteMutation = useMutation(useDeleteUserMutationOptions(''))
// ── Handlers ──────────────────────────────────────────────────────────────
async function handleCreate(formData: { email: string; name?: string }) {
try {
await createMutation.mutateAsync(formData)
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.users() })
} catch (err) {
if (err instanceof AuthkitClientError) {
alert(err.message)
}
}
}
async function handleDisable(id: string) {
setDisablingId(id)
try {
await disableMutation.mutateAsync()
// Invalidate both the list and the single-user query
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.users() })
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.user(id) })
} finally {
setDisablingId(null)
}
}
// ── Render ────────────────────────────────────────────────────────────────
if (isLoading) return <p>Loading…</p>
return (
<ul>
{data?.users.map((u) => (
<li key={u.id}>
{u.email}
<button onClick={() => handleDisable(u.id)}>Disable</button>
</li>
))}
</ul>
)
}Organization membership
Membership is where the hook-per-argument style pays off: the ids are bound when you call the hook, so the mutation function itself takes only what changes. That also means a hook bound to a specific member belongs in a component that renders that member — bind it in a row component, not once at the top of the page with a piece of state you have to remember to set.
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
authkitKeys,
useOrgQueryOptions,
useAddOrgMemberMutationOptions,
useCreateOrgInvitationMutationOptions,
useRemoveOrgMemberMutationOptions,
useUpdateOrgMemberRoleMutationOptions,
} from '@adonis-agora/authkit-react'
function MemberRow({ orgId, accountId, role }: { orgId: string; accountId: string; role: string }) {
const queryClient = useQueryClient()
const removeMember = useMutation(useRemoveOrgMemberMutationOptions(orgId, accountId))
const updateRole = useMutation(useUpdateOrgMemberRoleMutationOptions(orgId, accountId))
const refresh = () => queryClient.invalidateQueries({ queryKey: authkitKeys.admin.org(orgId) })
return (
<tr>
<td>{accountId}</td>
<td>
<select
value={role}
onChange={async (e) => {
await updateRole.mutateAsync(e.target.value)
refresh()
}}
>
<option value="member">member</option>
<option value="admin">admin</option>
</select>
</td>
<td>
<button
onClick={async () => {
await removeMember.mutateAsync()
refresh()
}}
>
Remove
</button>
</td>
</tr>
)
}
function OrgMembers({ orgId }: { orgId: string }) {
const queryClient = useQueryClient()
const { data: org } = useQuery(useOrgQueryOptions(orgId)) // org.members, org.pendingInvitations
const addMember = useMutation(useAddOrgMemberMutationOptions(orgId))
const invite = useMutation(useCreateOrgInvitationMutationOptions(orgId))
async function handleAdd(accountId: string) {
await addMember.mutateAsync({ accountId, role: 'member' })
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.org(orgId) })
}
async function handleInvite(email: string) {
const { invitation } = await invite.mutateAsync({ email, role: 'member' })
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.org(orgId) })
return invitation
}
return (
<table>
<tbody>
{org?.members.map((m) => (
<MemberRow key={m.accountId} orgId={orgId} accountId={m.accountId} role={m.role} />
))}
</tbody>
</table>
)
}Add a member when the person already has an account; create an invitation when they do not —
createInvitation takes an email, addMember takes an accountId. Revoking an invitation
(useRevokeOrgInvitationMutationOptions(orgId, invitationId)) is the mirror of creating one.
All five mutations touch the org detail, so authkitKeys.admin.org(orgId) is the key to
invalidate; add authkitKeys.admin.orgs() when the change alters the list (member counts, for
example).
Signing keys
client.admin.keys exposes the JWKS signing key: status() returns the current key age, the
rotation policy ({ enabled, maxAgeDays, keep }), how many days until the next rotation, and
the list of managed keys with their kid, algorithm, age, and which one is active.
rotate(input?) mints a new key, optionally retiring the old ones (retire) and capping how
many are kept (keep).
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
authkitKeys,
useKeysQueryOptions,
useRotateKeysMutationOptions,
} from '@adonis-agora/authkit-react'
function KeysPanel() {
const queryClient = useQueryClient()
const { data } = useQuery(useKeysQueryOptions())
const rotate = useMutation(useRotateKeysMutationOptions())
async function handleRotate() {
const result = await rotate.mutateAsync({ retire: false })
console.log(result.newKid, result.retiredKids, result.keptKids)
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.keys() })
}
if (!data) return null
return (
<div>
<p>Current key is {data.ageDays} days old.</p>
<p>
{data.policy.enabled
? `Rotates automatically every ${data.policy.maxAgeDays} days.`
: 'Automatic rotation is off.'}
</p>
<button onClick={handleRotate} disabled={rotate.isPending}>
Rotate now
</button>
</div>
)
}Rotating with retire: false keeps the previous keys published in the JWKS so tokens signed
with them still validate — retire them once those tokens have expired. The
KeyRotation component is this panel, already built; see
Signing key rotation for what happens server-side.
Query key reference
authkitKeys provides stable, hierarchical cache keys. Use them when calling
queryClient.invalidateQueries:
import { authkitKeys } from '@adonis-agora/authkit-react'
// Invalidate all admin data
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.all })
// Invalidate the users list (all search params)
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.users() })
// Invalidate a specific user + their sessions
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.user(id) })
queryClient.invalidateQueries({ queryKey: authkitKeys.admin.userSessions(id) })
// Account data
queryClient.invalidateQueries({ queryKey: authkitKeys.account.me() })
queryClient.invalidateQueries({ queryKey: authkitKeys.account.sessions() })All key shapes:
authkitKeys.admin.all // ['authkit', 'admin']
authkitKeys.admin.overview() // ['authkit', 'admin', 'overview']
authkitKeys.admin.users(params?) // ['authkit', 'admin', 'users', params]
authkitKeys.admin.user(id) // ['authkit', 'admin', 'users', id]
authkitKeys.admin.userSessions(id)
authkitKeys.admin.sessions(accountId?)
authkitKeys.admin.clients()
authkitKeys.admin.client(id)
authkitKeys.admin.roles()
authkitKeys.admin.orgs()
authkitKeys.admin.org(id)
authkitKeys.admin.audit(params?)
authkitKeys.admin.settings(orgId?) // ['authkit','admin','settings'] or [...,'settings', orgId]
authkitKeys.admin.impersonation(userId)
authkitKeys.admin.keys() // ['authkit', 'admin', 'keys']
authkitKeys.account.all
authkitKeys.account.me()
authkitKeys.account.security()
authkitKeys.account.sessions()
authkitKeys.account.apps()
authkitKeys.account.mfa()
authkitKeys.account.passkeys()
authkitKeys.account.tokens()
authkitKeys.account.orgs()
authkitKeys.account.org(id)
authkitKeys.account.orgInvitations()SSR and testing
In SSR environments window is not available. Pass options explicitly:
// Next.js server component or test
const client = createAuthkitClient({
baseUrl: process.env.AUTHKIT_ADMIN_API_URL,
accountBaseUrl: process.env.AUTHKIT_ACCOUNT_API_URL,
csrfToken: 'test-csrf',
})In tests, inject a mock fetch:
const client = createAuthkitClient({
baseUrl: 'http://localhost:3333/admin/api',
csrfToken: 'test',
fetch: vi.fn().mockResolvedValue(
new Response(JSON.stringify({ users: [], total: 0 }), { status: 200 })
),
})Account self-service API
The account API (/account/api/*) is a session-authenticated JSON API served by
@adonis-agora/authkit-server. It is not the admin API — it only exposes data and
actions for the currently signed-in user.
Authentication uses the account console session cookie (set at login). All mutating
endpoints require the X-CSRF-TOKEN header (the client sends it automatically). Sensitive
actions (change password, create/revoke PAT, remove passkey) additionally require an active
sudo session (authkit_sudo_at) — the server returns 403 sudo_required if the sudo
grace period has expired.
The account API is what client.account.* and all use* account hooks consume internally.
Routes registered by registerAuthHost (always on when the account console is mounted):
| Method | Path | Description |
|---|---|---|
GET | /account/api/me | Profile, capability flags, sudo state |
GET | /account/api/security | Sessions, MFA, passkeys, pending email change |
PATCH | /account/api/profile | Update name / avatar |
POST | /account/api/password | Change password (sudo + current password required) |
POST | /account/api/email-change | Request email change (sudo required) |
POST | /account/api/email-change/cancel | Cancel pending email change |
GET | /account/api/sessions | List active OIDC sessions |
DELETE | /account/api/sessions/:id | Revoke one session |
POST | /account/api/sessions/revoke-others | Revoke all other sessions |
POST | /account/api/sessions/revoke-all | Sign out of all devices (including current) |
GET | /account/api/apps | List authorized OAuth grants |
DELETE | /account/api/apps/:clientId | Revoke an app's access |
GET | /account/api/mfa | MFA status (TOTP + passkeys + recovery) |
GET | /account/api/passkeys | List passkeys |
DELETE | /account/api/passkeys/:id | Remove a passkey (sudo required) |
GET | /account/api/tokens | List Personal Access Tokens |
POST | /account/api/tokens | Create a PAT (sudo required; secret returned once) |
DELETE | /account/api/tokens/:id | Revoke a PAT (sudo required) |
GET | /account/api/orgs | List orgs the user belongs to |
GET | /account/api/orgs/invitations | List pending org invitations |
GET | /account/api/orgs/:id | Org detail (requires membership) |
All endpoints are capability-probed — if the underlying store doesn't support a feature
(e.g. passkeys, orgs, PATs), the route returns { supported: false, ... } rather than
erroring. This means the client can safely call any endpoint regardless of the specific
store configuration; check the supported field in the response before rendering controls.
Admin Console (dogfood)
The built-in React SPA admin console uses AuthkitClientProvider and all
the admin hooks internally — it is the canonical reference implementation. If you are
building a custom admin interface or embedding management panels in your app, use the same
hooks the console does. See Admin Console for the full route
table and how to mount it.