> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pool.day/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native and Expo

> Use PoolKit from a React Native or Expo app: install the module, set it up with the config plugin, and call the same API as Swift from TypeScript.

`@randomaccessmemories/pool-kit` wraps the Swift SDK as an Expo module. The API keeps the Swift names, so every guide in these docs applies: read the Swift example and call the same method from TypeScript. It runs on iOS 18 and later. Android is not supported, so guard your calls with `Platform.OS === "ios"`.

## Requirements

* Expo SDK 53 or later, with a [development build](https://docs.expo.dev/develop/development-builds/introduction/). Expo Go cannot load native modules.
* A bare React Native app works too, once it has [Expo modules installed](https://docs.expo.dev/bare/installing-expo-modules/).
* An app key from the [dashboard](https://pool.day/developers/dashboard), with your app's bundle ID added.

## 1. Install

```bash theme={null}
npx expo install @randomaccessmemories/pool-kit
```

Use 0.2.1 or later.

The module depends on the `PoolKit` pod, which ships the SDK as a binary XCFramework. `npx expo prebuild` and `pod install` fetch it for you.

## 2. Add the config plugin

The plugin writes the photo library sentence and the background task settings into your app's Info.plist at prebuild, raises the iOS deployment target to 18.0 (in `Podfile.properties.json` and the Xcode project, never lowering a higher one), and adds the `PoolKit` pod to your Podfile from the [pool-kit repo](https://github.com/ram-company/pool-kit). In a bare React Native app without prebuild, set `platform :ios, '18.0'` and add that line to your Podfile yourself:

```ruby Podfile theme={null}
pod 'PoolKit', :podspec => 'https://raw.githubusercontent.com/ram-company/pool-kit/0.2.0/PoolKit.podspec'
```

```json app.json theme={null}
{
  "expo": {
    "ios": { "bundleIdentifier": "com.example.app" },
    "plugins": [
      ["@randomaccessmemories/pool-kit", {
        "photoLibraryUsageDescription": "Find the things you saved, so we can suggest gifts you will like.",
        "backgroundIndexing": true
      }]
    ]
  }
}
```

| Option | Default | What it does |
| - | - | - |
| `photoLibraryUsageDescription` | "Find the things you saved in your photos. Your photos are read on this device." | The sentence iOS shows in the photo prompt. A value your app already sets is kept. |
| `backgroundIndexing` | `true` | Lets PoolKit keep indexing in the background. See [Indexing](https://docs.pool.day/indexing.md). |
| `backgroundTaskIdentifier` | `<bundle ID>.pool.index` | The background task PoolKit registers. |

With background indexing on, the plugin adds the task identifier, `<bundle ID>.pool.update` and `<bundle ID>.pool.continue.*` to `BGTaskSchedulerPermittedIdentifiers`, `processing` to `UIBackgroundModes`, and the identifier as `PoolKitBackgroundTaskIdentifier`. The module then registers the background tasks itself at launch, so there is no native code to write. A background task waits up to 20 seconds for `PoolKit.open`, so call it early, for example in your root component's first effect. See [Indexing](https://docs.pool.day/indexing.md#while-the-app-is-closed) for how runs are scheduled. Run `npx expo prebuild` after you change the plugin's options.

## 3. Open, ask for access, ask a question

```ts theme={null}
import PoolKit from "@randomaccessmemories/pool-kit";

await PoolKit.open({ appKey: "pk_test_…" });

const grant = await PoolKit.authorization.requestAccess({
  sources: ["screenshots", "photos"],
  data: ["products", "places"],
  purpose: "Suggest gifts from the things you saved",
});

const result = await PoolKit.context.query("sneakers I saved last month", grant);

for (const match of result.matches) {
  console.log(match.summary, match.provenance, match.confidence);
}

// An empty answer only means "nothing there" once every known photo is indexed.
const { indexedAssets, knownAssets } = result.coverage;
if (result.matches.length === 0 && indexedAssets < knownAssets) {
  console.log(`Still indexing: ${indexedAssets} of ${knownAssets}`);
}
```

Open once at launch and keep using the default export. Every call after `open` names the grant it runs under; pass the `Grant` object or its `id`. Store the id if you want to reuse a grant across launches, and read the live ones with `PoolKit.authorization.grants()`.

## How the API maps

| Swift | TypeScript |
| - | - |
| `PoolClient.open(configuration:)` | `PoolKit.open(config)` |
| `pool.authorization.requestAccess(_:)`, `revoke(_:)`, `grants()` | `PoolKit.authorization.requestAccess(request)`, `revoke(grant)`, `grants()` |
| `pool.context.query(_:using:)` | `PoolKit.context.query(question, grant, { limit, cursor })` |
| `pool.context.search(_:using:)` | `PoolKit.context.search(text, grant, { limit, offset, cursor })` |
| `similar`, `facets`, `recurringThings`, `evidence` | Same names, grant second |
| `ask`, `askUpdates` | `ask(question, grant)`, `askUpdates(question, grant, { onUpdate, onError })` |
| `profile`, `classify`, `interests`, `trips` | Same names, grant second |
| `people(using:)`, `photos(of:using:)` | `people(grant, options)`, `photosOf(groupId, grant, page)` |
| `pool.signals.subscribe(_:using:)` | `PoolKit.signals.subscribe({ rule } or { saved }, grant, { onSignal, onError })` |
| `pool.sources.status`, `statusUpdates` | `PoolKit.sources.status()`, `statusUpdates(onStatus)` |
| `indexNow`, `pause`, `resume`, `setIndexOptions`, `rebuildIndex`, `deleteIndex` | Same names on `PoolKit.sources` |
| `pool.sources.batches(using:)` | `PoolKit.sources.batches(grant, { onBatch, onError })` |
| `PoolClient.interestTaxonomy` | `PoolKit.interestTaxonomy()` |

Results are plain objects with the Swift field names. Dates are ISO 8601 strings. A Swift enum with a payload becomes an object keyed by its case, so `.growingFrom(.months(3))` is `{ growingFrom: { months: 3 } }`. All types are exported under their Swift names, such as `Grant`, `QueryResult`, `AssetSearchResult` and `IndexOptions`.

## Streams

`askUpdates`, `signals.subscribe`, `sources.statusUpdates` and `sources.batches` resolve to a stream. Call `remove()` when the screen goes away.

```ts theme={null}
const stream = await PoolKit.context.askUpdates("Which sneakers did I save most?", grant, {
  onUpdate: (update) => {
    switch (update.type) {
      case "searching":
        setStatus("Looking through your saves");
        break;
      case "writing":
        setText("");
        break;
      case "partial":
        setText(update.text); // The whole text so far, not a delta.
        break;
      case "answer":
        setAnswer(update.answer);
        break;
    }
  },
  onError: (error) => setError(error.code),
});

// Later, for example in a useEffect cleanup:
stream.remove();
```

```ts theme={null}
// Fill a grid while the first index runs. Each status carries `detail` and `lastRun`, as in Swift.
const progress = await PoolKit.sources.statusUpdates((status) => {
  setSearchable(status.detail.searchable);
  setProgress(status.detail.fraction);
});
const batches = await PoolKit.sources.batches(grant, {
  onBatch: (batch) => appendToGrid(batch.items),
});
```

```ts theme={null}
const signals = await PoolKit.signals.subscribe({ saved: "product" }, grant, {
  onSignal: (signal) => showNewSave(signal),
});
```

## Index options

Pass `indexOptions` to `open`, or change them later with `PoolKit.sources.setIndexOptions`. The fields match [Indexing](https://docs.pool.day/indexing.md).

```ts theme={null}
await PoolKit.open({
  appKey: "pk_live_…",
  indexOptions: {
    window: { growingFrom: { months: 3 } },
    mediaTypes: ["screenshots", "photos"],
    allowsCellular: false,
    backgroundProgress: { title: "Reading your saves" },
  },
});
```

## Errors

A rejected promise carries `code`, the name of the Swift `PoolError` case, and `message`, the case with its values, such as `rateLimited(retryAfter: 60)`. `PoolKit.errorDetails(error)` (also a named export) reads any rejection into `{ code, message, retryable, retryAfter?, serverCode? }`, the same fields as in [Errors](https://docs.pool.day/errors.md#retry-or-not). Stream `onError` handlers receive a `PoolKitStreamError` with those fields.

```ts theme={null}
import PoolKit, { errorDetails } from "@randomaccessmemories/pool-kit";

try {
  await PoolKit.context.ask("What did I save for the trip?", grant);
} catch (error) {
  const { code, retryable, retryAfter } = errorDetails(error);
  if (code === "cloudAssistNotAllowed") hideAskButton();
  else if (retryable) retryIn(retryAfter ?? 30);
  else showError(code);
}
```

## Without React Native

The same XCFramework is available to native apps that do not use Swift Package Manager.

> [tab] CocoaPods
>
> ```ruby Podfile theme={null}
> platform :ios, '18.0'
> use_frameworks!
>
> target 'YourApp' do
>   pod 'PoolKit', :podspec => 'https://raw.githubusercontent.com/ram-company/pool-kit/0.2.0/PoolKit.podspec'
> end
> ```
>
> The podspec comes from the [pool-kit repo](https://github.com/ram-company/pool-kit), not from the CocoaPods trunk, which stops taking new versions in December 2026. Change the version in the URL to upgrade.

> [tab] XCFramework
>
> Download `PoolKit.xcframework.zip` from the [latest release](https://github.com/ram-company/pool-kit/releases/latest), unzip it, and drag `PoolKit.xcframework` into your target's Frameworks, Libraries and Embedded Content with Embed and Sign. Link `libsqlite3.tbd` and `libc++.tbd`. The privacy manifest and the resource bundles are inside the framework.

The Swift package stays the main route, and the [Quickstart](https://docs.pool.day/index.md) uses it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.