v12.9.0

React Native SDK

Use Koolbase in your React Native app — auth (email, OAuth, phone), database, semantic search, storage with custom metadata, realtime, feature flags, functions, an offline write queue with conflict resolution, analytics and cloud messaging in one TypeScript package. The same core powers @koolbase/js in the browser.

Using Flutter? This page covers the React Native SDK. For Flutter (https://pub.dev/packages/koolbase_flutter), see the Flutter SDK guide.

Installation

npm install @koolbase/react-native
yarn add @koolbase/react-native
pnpm add @koolbase/react-native
bun add @koolbase/react-native

Optional peer dependency for persistent sessions

Install react-native-keychain (≥ 8.0.0) to enable secure session persistence (iOS Keychain + Android Keystore). Without it, the SDK runs without persistence rather than crashing — sessions are kept in memory only.

Setup

Initialize the SDK once at app startup, then restore any persisted session before rendering screens that depend on auth state.

App.tsx
TypeScript
import { useEffect, useState } from 'react';
import {
  Koolbase,
  RestoreResult,
} from '@koolbase/react-native';

export default function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      await Koolbase.initialize({
        publicKey: 'pk_live_your_key_here',
        baseUrl: 'https://api.koolbase.com',
        analyticsEnabled: true, // opt-in: off unless true (12.4.0+)
        appVersion: '1.0.0',
        authTimeout: 10000, // optional, default 10s
      });

      // Restore any session persisted from a previous launch.
      // Optimistic state is populated before any network call, so
      // authenticated UI renders immediately if a session exists.
      const result = await Koolbase.auth.restoreSession();
      switch (result) {
        case RestoreResult.Restored:
        case RestoreResult.Offline:    // optimistic restore, will revalidate
        case RestoreResult.NoSession:
        case RestoreResult.Expired:
          break;
      }
      setReady(true);
    })();
  }, []);

  if (!ready) return null;
  return <Navigation />;
}

Get your public key

Log in to app.koolbase.com, open your project, go to Environments, and copy the public SDK key.

Authentication

Email/password, social sign-in (Apple & Google), and phone + OTP all return a KoolbaseSession. Sessions persist across launches when react-native-keychain is installed.

Email + password

screens/AuthScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Register — password must be at least 8 characters (client-side check).
// Two outcomes: signed in, or account created and awaiting verification.
const result = await Koolbase.auth.register({
  email: '[email protected]',
  password: 'securepassword',
});
if (result.status === 'verification_required') {
  // The account exists. No session — they verify their email first.
  showCheckYourEmail(result.user.email);
} else {
  showApp();
}

// Login
const session = await Koolbase.auth.login({
  email: '[email protected]',
  password: 'securepassword',
});

// Attach user to analytics after login
Koolbase.analytics.identify(session.user.id);

// Current user (synchronous read of in-memory state)
const me = Koolbase.auth.currentUser;

// Logout — returns true if the server call succeeded; the local session
// is always cleared regardless.
await Koolbase.auth.logout();
Koolbase.analytics.reset();

Auth state listener

Subscribe to auth changes Firebase/Supabase style — fires immediately with the current state, then on every login, logout, or token refresh.

hooks/useUser.ts
TypeScript
import { useEffect, useState } from 'react';
import { Koolbase, KoolbaseUser } from '@koolbase/react-native';

export function useUser() {
  const [user, setUser] = useState<KoolbaseUser | null>(null);

  useEffect(() => {
    const unsubscribe = Koolbase.auth.onAuthStateChange((state) => {
      setUser(state.user);
    });
    return () => unsubscribe();
  }, []);

  return user;
}

Social sign-in

Koolbase is library-agnostic: get the identity token from any native sign-in package, and the server verifies it against the provider's JWKS.

screens/AuthScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Apple — identityToken comes from a native Apple Sign-In package
// (e.g. @invertase/react-native-apple-authentication).
const appleSession = await Koolbase.auth.signInWithApple({
  identityToken,
  nonce,
  // fullName is only available on the user's FIRST sign-in
  fullName: { givenName: 'Ada', familyName: 'Lovelace' },
});

// Google — idToken comes from a native Google Sign-In package
// (e.g. @react-native-google-signin/google-signin).
const googleSession = await Koolbase.auth.signInWithGoogle({ idToken });

Auto-link policy

A social identity attaches to an existing user only when both emails are verified and match (case-insensitive). Otherwise the SDK creates a new user, or surfaces OAuthEmailConflictError if linkage is ambiguous.

Phone + OTP

screens/PhoneAuthScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// 1. Send a one-time code over SMS (E.164 format required)
const { expiresAt } = await Koolbase.auth.sendOtp({
  phoneNumber: '+233200000000',
});

// 2. Verify the code to sign in (creates the user if new)
const { session, isNewUser } = await Koolbase.auth.verifyOtp({
  phoneNumber: '+233200000000',
  code: '123456',
});

// Or link a phone number to the currently signed-in user
await Koolbase.auth.linkPhone({
  phoneNumber: '+233200000000',
  code: '123456',
});

Password reset

screens/ForgotPasswordScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// 1. Trigger the reset email
await Koolbase.auth.forgotPassword({ email: '[email protected]' });

// 2. Consume the token from the email link to set a new password
await Koolbase.auth.resetPassword({
  token: 'token-from-email-link',
  password: 'newSecurePassword',
});

Account unlock

After too many failed login attempts, an account is locked and the user receives an unlock email with a one-shot token.

screens/UnlockScreen.tsx
TypeScript
await Koolbase.auth.unlock('token-from-unlock-email');

See Authentication for the full auth reference, including session lifecycle and typed errors.

Database

CRUD against typed collections. insert and update and delete are online-first with offline fallback — the authoritative server record on success, queued locally on network failure. A server rejection is never queued: it surfaces immediately, since it would be refused again on every retry.

screens/PostsScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Insert — returns the authoritative server record on success
const post = await Koolbase.db.insert('posts', {
  title: 'Hello Koolbase',
  published: true,
});

// Query
const { records, total } = await Koolbase.db.query('posts', {
  filters: { published: true },
  limit: 10,
  orderBy: 'created_at',
  orderDesc: true,
});

// Populate related records (one or many)
const { records: postsWithAuthor } = await Koolbase.db.query('posts', {
  populate: ['author_id:users', 'category_id:categories'],
});

// Get by id
const single = await Koolbase.db.get('record-id');

// Update
await Koolbase.db.update('record-id', { title: 'Updated' });

// Delete
await Koolbase.db.delete('record-id');

Upsert & bulk delete

screens/SettingsScreen.tsx
TypeScript
// Upsert — insert or update by a match filter; online-only.
// Throws KoolbaseConflictError on unique-constraint violation.
const { record, created } = await Koolbase.db.upsert(
  'user_settings',
  { user_id: userId },               // match
  { user_id: userId, theme: 'dark' } // data
);

// Bulk delete by filter — returns the number of records deleted.
const removed = await Koolbase.db.deleteWhere('notifications', {
  user_id: userId,
  read: true,
});

Batch transactions

Run multiple writes as a single atomic transaction — all operations commit together or none are applied. Online-only by design; throws on network failure rather than queuing.

screens/CheckoutScreen.tsx
TypeScript
import { Koolbase, BatchOp } from '@koolbase/react-native';

const results = await Koolbase.db.batch([
  BatchOp.insert('orders',     { user_id: userId, total: 4999 }),
  BatchOp.update('cart',       cartId, { checked_out: true }),
  BatchOp.upsert('inventory',  { sku: 'A1' }, { sku: 'A1', count: -1 }),
  BatchOp.delete('cart_items', itemId),
]);

// One BatchResult per operation, in order.
results.forEach(r => console.log(r));

Typed data errors

Database operations throw subclasses of KoolbaseDataError selected from the server's stable error code: KoolbaseNotFoundError, KoolbaseValidationError, KoolbasePermissionError, KoolbaseRateLimitError, and KoolbaseConflictError (carries the offending field). See Database for the full reference.

Offline-first database

Queries return cached data instantly and refresh from the network in the background. insert, update, and delete queue on network failure and replay on reconnect — an update or delete only if the record has been read on this device, so the change has something to be applied against. A write the server refuses is held for a decision, with the server's message, and nothing is ever dropped.

screens/PostsScreen.tsx
TypeScript
const { records, isFromCache } = await Koolbase.db.query('posts', {
  filters: { published: true },
  limit: 20,
});

if (isFromCache) console.log('Served from local cache');

// Manual sync — usually unnecessary, NetInfo triggers it on reconnect
await Koolbase.db.syncPendingWrites();

The full model — baselines, conflict reasons, the four resolutions, and what happens to each kind of failure — is documented once at Offline Support, with React Native examples throughout.

Semantic Search

Search records by meaning, not by field equality. Configure a vector field on a collection with an embedding provider (Gemini or OpenAI) and a source field — records are auto-embedded on every insert and update. At query time, pass the user's question as plain text; the server embeds it through the same provider before running HNSW similarity search. Zero embedding code on the client.

Search with text

Most common usage. The server embeds the query inline using the vector field's configured provider — no client-side embedding model required.

screens/FaqScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

const result = await Koolbase.db.searchSemantic({
  collection: 'faqs',
  field: 'question_embedding',
  queryText: 'how do I cancel my subscription?',
  limit: 5,
  // Optional: equality filter applied alongside vector search
  where: { is_published: true },
});

// result.hits is SemanticHit[] — each carries record + cosine distance.
// Lowest distance = closest match. For display: similarity = 1 - distance.
result.hits.forEach((hit) => {
  console.log(hit.record, 'distance:', hit.distance);
});
console.log(`${result.total} matches`);

Search with a pre-computed vector

Use when you've already embedded the query on the client (advanced; most apps should use queryText).

screens/FaqScreen.tsx
TypeScript
const result = await Koolbase.db.searchSemantic({
  collection: 'faqs',
  field: 'question_embedding',
  queryVector: precomputedVector,
  limit: 5,
});

Manual vector operations

When you need finer control — your own embedding model, hybrid storage strategies, or testing. Manual writes override any auto-embedded vector on the same record.

screens/AdminScreen.tsx
TypeScript
// Write a vector for a record. The field must already be declared on
// the collection; the vector's length must match its dimension.
await Koolbase.db.setVector(recordId, 'question_embedding', vector);

// Read it back as KoolbaseVector { recordId, fieldName, vector, createdAt, updatedAt }
const stored = await Koolbase.db.getVector(recordId, 'question_embedding');
console.log(`${stored.vector.length}-dim, last updated ${stored.updatedAt}`);

// Remove a record's vector slot (the field declaration stays intact)
await Koolbase.db.deleteVector(recordId, 'question_embedding');

For backfilling embeddings on records that pre-date the auto-embed hook, use Koolbase.db.embedText({recordId, field, text?}). See Vectors for the full signature.

Vector fields are declared from the dashboard or CLI

The mobile SDK doesn't declare schema. Open Database → your collection → Vector Fields in the dashboard, or run koolbase vector-fields create <collection> <name> --dimensions 1536 --provider gemini --model gemini-embedding-001 --source-field <field>. Supported dimensions: 384, 768, 1024, 1536.

Online-only

Vector operations aren't cached or queued offline — HNSW similarity search has no useful offline semantics. KoolbaseVectorDimensionMismatchError is thrown when a setVector call's length doesn't match the declared dimension.

See Vectors for the full reference, or walk through the RAG Knowledge Base tutorial for an end-to-end FAQ assistant.

Storage

Upload files to R2-backed buckets, attach custom key/value metadata, and retrieve them via short-lived signed download URLs. The SDK runs a presigned 3-step flow under the hood (presign → R2 PUT → confirm) so file bytes go directly to R2 without round-tripping through Koolbase.

screens/ProfileScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Upload — returns { object, downloadUrl }
const { object, downloadUrl } = await Koolbase.storage.upload({
  bucket: 'avatars',
  path: `user-${userId}.jpg`,
  file: {
    uri: imageResult.assets[0].uri,
    name: 'avatar.jpg',
    type: 'image/jpeg',
  },
  // Optional: attach custom metadata at upload time
  metadata: {
    user_id: userId,
    orig_filename: 'IMG_0942.HEIC',
    content_version: '1',
  },
  // Safe-by-default: rejects existing paths unless overwrite is true
  overwrite: false,
});

console.log(object.id, object.size, object.metadata);

// Signed download URL on demand
const url = await Koolbase.storage.getDownloadUrl(
  'avatars',
  `user-${userId}.jpg`,
);

// Delete
await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);

Update metadata

Edit metadata independently of the file bytes. Uses merge semantics — string values set or update keys, null deletes them, omitted keys are left as-is.

screens/PhotoEditScreen.tsx
TypeScript
await Koolbase.storage.updateMetadata('avatars', `user-${userId}.jpg`, {
  alt_text: 'Profile photo',
  orig_filename: null,         // delete this key
  // any other existing key is left untouched
});

Typed storage errors

Storage operations throw KoolbaseStorageError subclasses with one branch per failure mode: KoolbaseStorageConflictError (path collision when overwrite is false), KoolbaseStorageQuotaError (bucket size cap), KoolbaseStorageFileTooLargeError (per-file size cap), KoolbaseStorageMimeTypeError (content-type allowlist), and KoolbaseStorageMetadataInvalidError (metadata validation — exposes a detail field naming the failing key and rule). See Custom Metadata for the validation budget.

Realtime

Stream created, updated, and deleted events from a collection. Auto-reconnects; authenticated as the signed-in user.

screens/FeedScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

useEffect(() => {
  const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
    if (event.type === 'created') {
      setMessages(prev => [event.record!, ...prev]);
    }
    if (event.type === 'updated') {
      setMessages(prev =>
        prev.map(m => (m.id === event.record!.id ? event.record! : m)),
      );
    }
    if (event.type === 'deleted') {
      // deleted events don't carry a record — use recordId
      setMessages(prev => prev.filter(m => m.id !== event.recordId));
    }
  });

  return () => unsubscribe();
}, []);

Functions

Invoke deployed Functions. The signed-in user's access token is forwarded automatically — Functions receive caller identity via ctx.auth.

screens/CheckoutScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

const result = await Koolbase.functions.invoke('process-payment', {
  amount: 4999,
  currency: 'USD',
  userId: user.id,
});

if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

See Invoke from Flutter / RN for the auth model, retries, and error shapes.

Feature flags & remote config

screens/HomeScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Feature flag
if (Koolbase.isEnabled('new_checkout')) {
  // show new checkout UI
}

// Remote config — typed accessors with required defaults
const timeout = Koolbase.configNumber('timeout_seconds', 30);
const apiUrl  = Koolbase.configString('api_url', 'https://api.myapp.com');
const dark    = Koolbase.configBool('force_dark_mode', false);

// Version enforcement
const result = Koolbase.checkVersion('1.2.3');
if (result.status === 'force_update') {
  // block the app and show update screen
}

Analytics

Track screen views, custom events, and user behaviour. Events are batched and flushed automatically — no manual setup required beyond enabling analytics in your config.

screens/CheckoutScreen.tsx
TypeScript
import { Koolbase } from '@koolbase/react-native';

// Custom event
Koolbase.analytics.track('purchase', {
  value: 1200,
  currency: 'GHS',
  product_id: 'prod_123',
});

// Screen view
Koolbase.analytics.screenView('checkout');

// User identity — call after login
Koolbase.analytics.identify(user.id);
Koolbase.analytics.setUserProperty('plan', 'pro');
Koolbase.analytics.setUserProperties({ plan: 'pro', country: 'GH' });

// On logout
Koolbase.analytics.reset();

// Manual flush
await Koolbase.analytics.flush();

Auto events

On initialize, the SDK automatically tracks app_open. Call screenView() on each navigation. session_end fires when dispose() is called.

Batch flush triggers

Events are flushed automatically: every 30 seconds, when the app goes to background (via AppState), when the queue reaches 20 events, and when dispose() is called. Failed flushes are re-queued and retried.

Cloud Messaging

Register a device's push token with Koolbase so you can deliver notifications. The token comes from your native push library (for example @react-native-firebase/messaging) — Koolbase stores it against the device and, optionally, the signed-in user.

App.tsx
TypeScript
import { Platform } from 'react-native';
import { Koolbase } from '@koolbase/react-native';

export const metadata = {
  title: 'React Native — SDK',
  description: 'Use Koolbase in your React Native app — auth (email, OAuth, phone), database, semantic search, storage with custom metadata, realtime, feature flags, functio...',
}

// Register the device's push token (obtained from your native FCM library)
await Koolbase.messaging.registerToken({
  token: fcmToken,
  platform: Platform.OS === 'ios' ? 'ios' : 'android',
  userId: user.id, // optional — associates the token with a user
});
screens/AdminScreen.tsx
TypeScript
// Send a notification to a specific device token
await Koolbase.messaging.send({
  to: fcmToken,
  title: 'New message',
  body: 'You have a new message',
  data: { screen: 'inbox' },
});

Delivery

Notifications are delivered through your project's configured FCM credentials. Both registerToken and send return a boolean indicating whether the request succeeded.

Error handling

Every domain throws typed errors that extend a domain base class — so you can branch on the specific failure mode, or fall back to the base class for catch-all handling. No string matching required.

screens/CheckoutScreen.tsx
TypeScript
import {
  KoolbaseAuthError,
  InvalidCredentialsError,
  AccountLockedError,
  RateLimitError,
  KoolbaseDataError,
  KoolbaseConflictError,
  KoolbaseStorageError,
  KoolbaseStorageConflictError,
  KoolbaseStorageMetadataInvalidError,
} from '@koolbase/react-native';

try {
  await Koolbase.auth.login({ email, password });
} catch (err) {
  if (err instanceof InvalidCredentialsError) {
    // wrong email/password
  } else if (err instanceof AccountLockedError) {
    // err.lockedUntil — show "try again at X" message
  } else if (err instanceof RateLimitError) {
    // too many attempts
  } else if (err instanceof KoolbaseAuthError) {
    // any other auth failure
  }
}

try {
  await Koolbase.storage.upload({ bucket, path, file, overwrite: false });
} catch (err) {
  if (err instanceof KoolbaseStorageConflictError) {
    // path already exists — prompt to overwrite
  } else if (err instanceof KoolbaseStorageMetadataInvalidError) {
    console.error(err.detail);  // names the failing key + rule
  } else if (err instanceof KoolbaseStorageError) {
    // any other storage failure
  }
}

Error hierarchy

Auth: KoolbaseAuthError ← InvalidCredentialsError, EmailAlreadyInUseError, UserDisabledError, WeakPasswordError, SessionExpiredError, TokenRevokedError, AccountLockedError, UnlockTokenInvalidError, RateLimitError, NetworkError, OAuth + OTP errors.

Database: KoolbaseDataError ← KoolbaseNotFoundError, KoolbaseValidationError, KoolbasePermissionError, KoolbaseRateLimitError, KoolbaseConflictError.

Storage: KoolbaseStorageError ← KoolbaseStorageNotFoundError, KoolbaseStorageValidationError, KoolbaseStoragePermissionError, KoolbaseStorageConflictError, KoolbaseStorageQuotaError, KoolbaseStorageFileTooLargeError, KoolbaseStorageMimeTypeError, KoolbaseStorageMetadataInvalidError.

See the Errors reference for wire codes and HTTP statuses.

Source & contributing

The React Native SDK is open source at github.com/kennedyowusu/koolbase-react-native. Issues and PRs welcome.