Skip to content

React Native

Ship a vector and document database inside your mobile app. Store application data and embeddings from a local model, then run structured queries, vector search, BM25 full-text, and hybrid RAG retrieval directly on the phone — private by default and available offline.

TalaDB runs natively on iOS and Android via a JSI integration — calls from JavaScript go directly into the Rust engine without bridge overhead or JSON serialisation on the hot path. Large reads and vector searches are dispatched to a background thread so they do not block the JS thread.

Requirements

  • React Native 0.73+ with New Architecture enabled
  • Expo SDK 50+ (if using Expo)
  • Xcode 15+ for iOS builds
  • Android NDK r27+ for Android builds

Installation

bash
pnpm add taladb @taladb/react-native

iOS

bash
cd ios && pod install

Android

No extra steps — Gradle links the native library automatically.

Enable the New Architecture

TalaDB requires the New Architecture for its JSI integration.

android/gradle.properties

properties
newArchEnabled=true

ios/Podfile

ruby
use_framework! :static

Quick start

Call TalaDBModule.initialize once at app startup — before any component tries to use the database.

ts
// App.tsx
import { TalaDBModule } from '@taladb/react-native'
import { openDB } from 'taladb'

await TalaDBModule.initialize('myapp.db')

const db = await openDB('myapp.db')
const users = db.collection('users')

await users.insert({ name: 'Alice', createdAt: Date.now() })
const all = await users.find()

That's it. The taladb package detects React Native automatically — the same code you write for the browser or Node.js works here too.

Import openDB from taladb, not from @taladb/react-native. The latter is the low-level binding: it installs the native module and exposes the raw JSI surface synchronously, without migrations, live queries, or schema handling. Use it directly only when you specifically want synchronous calls on the JS thread.

Change webhook

Report every committed write to a backend over HTTP. Configure it on openDB — not on TalaDBModule.initialize, which now carries only storage settings (durability, encryption passphrase):

ts
import { TalaDBModule } from '@taladb/react-native'
import { openDB } from 'taladb'

await TalaDBModule.initialize('myapp.db')

const db = await openDB('myapp.db', {
  webhook: {
    enabled: true,
    endpoint: 'https://api.example.com/taladb',
    headers: { Authorization: `Bearer ${myToken}` },
    exclude_fields: ['embedding'],  // omit large vector fields
  },
})
// Every write now fires an HTTP request after the commit

POST on insert, PUT on update, DELETE on delete. Delivery happens after the commit on a bounded in-memory queue with 3 retries and exponential backoff (200 / 400 / 800 ms) on 5xx and network errors. The write path is never blocked; a saturated queue drops events rather than applying back-pressure.

Observe delivery health and drain before logout or backgrounding:

ts
db.webhookStats?.()          // { pending, delivered, failed, dropped }
await db.flushWebhook?.(5_000)
await db.close()             // drains automatically

The webhook is a best-effort event stream, not a durable replication queue. A crash can lose events and a retry can duplicate one; retries reuse event_id and Idempotency-Key so the receiver can deduplicate. If events must survive process termination, use an outbox or reconciliation pass.

Per-op endpoint overrides are supported:

ts
const db = await openDB('myapp.db', {
  webhook: {
    enabled: true,
    endpoint: 'https://api.example.com/events',
    insert_endpoint: 'https://api.example.com/events/insert',
    update_endpoint: 'https://api.example.com/events/update',
    delete_endpoint: 'https://api.example.com/events/delete',
  },
})

Delivery runs in the taladb TypeScript client on fetch, so it behaves exactly as it does on the browser and Node.js. See the Change Webhook reference.

Full example

tsx
// App.tsx
import React, { useEffect, useState } from 'react'
import { View, Text, Button, FlatList } from 'react-native'
import { TalaDBModule } from '@taladb/react-native'
import { openDB, type Collection } from 'taladb'

interface Note {
  _id?: string
  text: string
  createdAt: number
}

let notes: Collection<Note>

export default function App() {
  const [items, setItems] = useState<Note[]>([])

  useEffect(() => {
    async function init() {
      await TalaDBModule.initialize('notes.db')
      const db = await openDB('notes.db')
      notes = db.collection<Note>('notes')
      await notes.createIndex('createdAt')
      setItems(await notes.find())
    }
    init()
  }, [])

  async function addNote() {
    await notes.insert({ text: `Note ${Date.now()}`, createdAt: Date.now() })
    setItems(await notes.find())
  }

  return (
    <View style={{ flex: 1, padding: 40 }}>
      <Button title="Add Note" onPress={addNote} />
      <FlatList
        data={items}
        keyExtractor={(item) => item._id!}
        renderItem={({ item }) => <Text>{item.text}</Text>}
      />
    </View>
  )
}

TalaDB supports on-device semantic search — store embeddings from a local ML model (Core ML, TensorFlow Lite) and search them without any server.

The API is the same one you use on the browser and Node.js: promise-based, with heavy work dispatched to a background thread by the native module rather than blocking the JS thread.

ts
interface Article {
  _id?: string
  title: string
  body: string
  embedding: number[]
}

const articles = db.collection<Article>('articles')
await articles.createVectorIndex('embedding', { dimensions: 384 })

// Insert with an embedding from your on-device model
const embedding = await myModel.embed(content)
await articles.insert({ title, body: content, embedding })

// Semantic search
const queryVec = await myModel.embed(userQuery)
const results = await articles.findNearest('embedding', queryVec, 5)

results.forEach(({ document, score }) => {
  console.log(score.toFixed(3), document.title)
})

// Filtered vector search: metadata filter applied before ranking
const filtered = await articles.findNearest('embedding', queryVec, 5, {
  category: 'faq',
})

Passing a Float32Array as the query vector takes a zero-copy path across JSI.

Persistent approximate search on mobile

React Native uses the same transactional HNSW implementation as browser and Node. Graph nodes and links live in the database; app restarts require no warm-up. Inserts, embedding updates and deletes maintain the index atomically.

ts
await articles.createVectorIndex('embedding', {
  dimensions: 384, indexType: 'hnsw', hnswM: 16, quantization: 'scalar',
})
const results = await articles.searchVectors('embedding', queryVector, 10,
  { category: 'notes' }, { mode: 'ann', efSearch: 200, groupBy: 'parentId' })

Advanced vector operations use the native background executor. For a large rebuild or flat-to-HNSW promotion, use rebuildVectorIndex with batchSize, onProgress and signal; the default batch is 32 insertions and cancellation takes effect between batches. beginVectorBuild/stepVectorBuild let the app resume a persisted build after interruption. The existing graph stays available until the replacement commits.

Graph record caching is bounded at 8 MiB per operation. This excludes query queues, result documents, the database page cache and original vectors. Scalar/binary quantization compresses graph vectors; measure memory, recall and latency on your actual mobile devices. All scores are rescored from full precision originals.

The direct @taladb/react-native collection also exposes the asynchronous vector methods. Its legacy createVectorIndex and upgradeVectorIndex methods are synchronous; use batched rebuildVectorIndex when working with an existing large collection. See the complete vector API.

BM25 full-text ranking needs an FTS index on the field:

ts
await articles.createFtsIndex('body')

const hits = await articles.searchText('body', 'reset my password', 5)

hybridSearch runs both retrievers and fuses their rankings with reciprocal rank fusion. The two fail differently — keyword search misses paraphrases, vector search misses exact identifiers and rare proper nouns — so fusing them recovers both. It needs an FTS index on the text field and a vector index on the vector field:

ts
const hits = await articles.hybridSearch(
  { textField: 'body', text: userQuery },
  { vectorField: 'embedding', vector: queryVec },
  5,
)

hits.forEach(({ document, score, textRank, vectorRank }) => {
  // textRank / vectorRank are null when that retriever did not return the row
  console.log(document.title, textRank, vectorRank)
})

The fused score is meaningful only as an ordering inside one result set — it is not a similarity or a confidence.

Where data is stored

PlatformLocation
iOSNSDocumentDirectory (iCloud-excluded by default)
AndroidContext.getFilesDir() (app-private, no permissions needed)

No special permissions are required on either platform.

Migrations

ts
const db = await openDB('myapp.db', {
  migrations: [
    {
      version: 1,
      description: 'Add notes index',
      up: async (db) => {
        await db.collection('notes').createIndex('createdAt')
      },
    },
  ],
})

Troubleshooting

__TalaDB__ JSI HostObject not foundopenDB was called before TalaDBModule.initialize completed. Move initialize to the very top of your app entry point and await it before any database access.

TurboModuleRegistry.getEnforcing('TalaDB'): 'TalaDB' could not be found (Android) The native module was not linked. Verify that your android/gradle.properties has newArchEnabled=true and that you are using a custom dev client@taladb/react-native cannot run inside Expo Go.

New Architecture is not enabled Set newArchEnabled=true in android/gradle.properties and add use_framework! :static to your ios/Podfile.

Pod install fails on iOS Make sure Xcode command-line tools are active: xcode-select --install. Then re-run pod install.

Current limitations

  • Expo Go — not supported. You must use a custom dev client (expo prebuild).
  • Live queries (subscribe) — polling-based on React Native; native file-watch push is planned for a future release.
  • ANN recall depends on the workload — use measureVectorRecall and test latency/memory on target devices.
  • Original vectors are stored as f32 — scalar or binary quantization compresses the HNSW graph vectors, while originals remain available for exact search and rescoring.