Skip to content

@kiavi/kiavi-react-native

Current version: 0.1.0-alpha.1

React Native client for Kiavi-hosted authentication. Use this in Expo or bare React Native apps (iOS and Android) when you need to sign users in against a Kiavi auth server and attach their access token to outgoing API calls.

This page mirrors the llm-docs.md that ships with the installed package, that file is the authoritative source. If anything here ever drifts, trust the package version.

Speaks the same /api/auth/exchange/* wire protocol as @kiavi/kiavi-browser. PKCE only, no client secrets. Refresh tokens are stored in the OS keychain (iOS Keychain / Android Keystore) via expo-secure-store.

Terminal window
pnpm add @kiavi/kiavi-react-native expo-auth-session expo-secure-store expo-crypto

expo-auth-session brings expo-web-browser in transitively. If you have a bare workflow that pins peer deps, install it explicitly too.

Then run the init command to install the version-pinned integration guide for AI coding agents into your repo:

Terminal window
npx kiavi-react-native init

This writes .kiavi/kiavi-react-native.md and adds a reference block to your AGENTS.md (or CLAUDE.md). Re-run on every upgrade. See AI agent docs for the full explanation.

The SDK derives its redirect URI from clientId by stripping a trailing -native suffix: yourapp-nativeyourapp://auth. Add the matching scheme to app.json:

{
"expo": {
"scheme": "yourapp",
"ios": { "bundleIdentifier": "com.yourapp.app" },
"android": { "package": "com.yourapp.app" }
}
}

If you need a different scheme or a Universal Link / App Link, pass redirectUri explicitly. Whatever you pass must match an entry in the client’s allowed_origins (configured in the Kiavi management UI). The auth server only allows the derived scheme prefix for that client.

If your clientId does not end in -native, the constructor throws KiaviAuthError with code: 'invalid_request', pass redirectUri explicitly to opt out of derivation.

Three things, in order:

  1. Create one KiaviClient instance and export it.
  2. Call authenticate() from a sign-in button to ensure the user is signed in. This opens the system browser if needed.
  3. Call getAccessToken() before each API call and attach it as a Bearer token.
lib/kiavi.ts
import { KiaviClient } from '@kiavi/kiavi-react-native'
export const kiavi = new KiaviClient({
authBaseUrl: 'https://yourapp.kiavi.eu',
clientId: 'yourapp-native',
// redirectUri: 'yourapp://auth', derived automatically
})
// sign-in screen
import { Button } from 'react-native'
import { kiavi } from '@/lib/kiavi'
import { useRouter } from 'expo-router'
export default function SignInScreen() {
const router = useRouter()
return (
<Button
title='Sign in'
onPress={async () => {
await kiavi.authenticate()
router.replace('/home')
}}
/>
)
}
// any API call
import { kiavi } from '@/lib/kiavi'
const token = await kiavi.getAccessToken()
const res = await fetch('https://api.yourapp.com/me', {
headers: { Authorization: `Bearer ${token}` },
})

That is the full happy path. authenticate() is idempotent: if a session is already live or silently refreshable from secure storage, it returns immediately without opening the browser. Otherwise it opens the system browser (SFSafariViewController on iOS, Custom Tabs on Android) and resolves once the user returns via the deep link.

new KiaviClient({
authBaseUrl: 'https://yourapp.kiavi.eu',
clientId: 'yourapp-native',
redirectUri: 'yourapp://auth', // optional, derived from clientId by default
debug: true, // logs to console; pass a function for structured routing
})
  • authBaseUrl: string, required. The base URL of the Kiavi auth server.
  • clientId: string, required. The OIDC client ID registered for this app. Must end in -native if you want the redirect URI auto-derived.
  • redirectUri?: string, override the default ${scheme}://auth derived from clientId. Use this for Universal Links / App Links or other deep-link conventions. Must match an entry in the client’s allowed_origins.
  • debug?: boolean | KiaviDebugLogger, true enables the built-in console logger. A function receives structured KiaviDebugLogEntry objects so you can route logs to your own observability tooling.

Ensure the user is signed in. Resolution order:

  1. Live in-memory session that is not expiring soon, return it.
  2. Refresh token in secure storage, silently mint a new access token and return.
  3. Otherwise, open the system browser to the authorize endpoint, wait for the deep-link callback, exchange the code, and persist the new refresh token.

Concurrent callers share a single in-flight authenticate() promise. Throws KiaviAuthError if the browser session is dismissed, the redirect is malformed, or the server rejects the exchange.

Return a valid JWT for an Authorization header. Refreshes automatically when the access token has less than 30 seconds of life left. Concurrent callers share one in-flight refresh.

Throws KiaviSessionExpiredError if there is no session and refresh fails. Catch that and call authenticate() to start a new sign-in flow.

import { KiaviSessionExpiredError } from '@kiavi/kiavi-react-native'
try {
const token = await kiavi.getAccessToken()
// ...
} catch (err) {
if (err instanceof KiaviSessionExpiredError) {
await kiavi.authenticate()
} else {
throw err
}
}

getSession(): Promise<KiaviSession | null>

Section titled “getSession(): Promise<KiaviSession | null>”

Non-redirecting read of the current session. Awaits any in-flight initialization, then returns the live session or null. Use this on app launch to decide whether to render the signed-in or signed-out shell without triggering the browser.

Revokes the refresh token server-side, wipes secure storage, and notifies listeners. There is no browser redirect on mobile, handle navigation in your onAuthStateChange listener or after signOut() resolves.

Subscribe to session changes. Fires on every successful sign-in, every refresh, every sign-out, and every refresh failure that clears the session (for example, the user revoked this device from another device). Does not fire synchronously on subscription, call getSession() first if you need the current state. Returns an unsubscribe function.

useEffect(() => {
return kiavi.onAuthStateChange((session) => {
if (!session) router.replace('/sign-in')
})
}, [])
import {
KiaviAuthError,
KiaviSessionExpiredError,
KiaviBrowserOnlyError,
} from '@kiavi/kiavi-react-native'

Thrown by authenticate(), getAccessToken() (during refresh), and signOut() (during revoke) on any non-success response. Fields:

  • code: KiaviAuthErrorCode, 'rate_limited' | 'invalid_request' | 'unauthenticated' | 'server_error' | 'network_error' | 'unknown'
  • status: number, HTTP status, or 0 for network errors / invalid input
  • retryAfterSeconds?: number, parsed from the Retry-After header on 429 responses
  • message: string, server-provided message when available, otherwise a generic description

Network errors (offline, DNS failure) surface as KiaviAuthError with code: 'network_error' and status: 0. They never wipe the user’s saved refresh token, only an unauthenticated (401) response from /refresh does that, since it means the chain has been server-side revoked.

Thrown by getAccessToken() when there is no session and refresh cannot recover one. Catch and call authenticate() to start a new sign-in flow.

Exported for cross-SDK API parity with @kiavi/kiavi-browser. The native SDK never throws this; it exists so consumer code that catches both can compile against either package.

Exported from @kiavi/kiavi-react-native:

  • KiaviClient, the class above
  • KiaviSession, { token: string, expiresAt: number | null, user: KiaviSessionUser | null }
  • KiaviSessionUser, { id: string, email: string, emailVerified?: boolean, name: string | null }
  • KiaviClientOptions, KiaviAuthenticateOptions
  • KiaviAuthStateListener, (session: KiaviSession | null) => void
  • KiaviAuthError, KiaviAuthErrorCode
  • KiaviSessionExpiredError
  • KiaviBrowserOnlyError (parity-only, never thrown by this package)
  • KiaviDebugLogger, KiaviDebugLogEntry

Do:

  • Create exactly one KiaviClient instance per app and export it as a module singleton.
  • Call getAccessToken() inline on every API request, it’s fast and handles refresh.
  • Call getSession() on app launch to decide between signed-in and signed-out shells without triggering the browser.
  • Let authenticate() be the only entry point that may open the browser.
  • Pair this with @kiavi/kiavi-js on your backend to verify the JWTs this client produces.

Do not:

  • Do not implement your own refresh loop or token cache. getAccessToken() is already silent, automatic, and de-duped across concurrent callers.
  • Do not write the refresh token anywhere yourself, the client owns secure storage.
  • Do not catch KiaviAuthError with code: 'network_error' and clear local state; that’s just the user being offline. Only unauthenticated from /refresh indicates a revoked chain, and the client already handles that internally.
  • Do not pass clientId values that don’t end in -native without also passing redirectUri, the constructor will throw.

expo-auth-session, expo-secure-store, expo-crypto, and expo-web-browser all support bare RN via expo install. Follow each package’s installation steps; no other changes are needed.