-
Notifications
You must be signed in to change notification settings - Fork 93
feat(react-native): Expo module for on-device Moss (#432) #473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
samanyugoyal2010
wants to merge
10
commits into
usemoss:main
Choose a base branch
from
samanyugoyal2010:issue-432-react-native-expo
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
c9d765b
feat(react-native): add Expo module for on-device Moss (#432)
samanyugoyal2010 5bc4c70
fix(react-native): harden iOS CocoaPods integration for Expo SDK 57
samanyugoyal2010 73b5717
Merge remote-tracking branch 'origin/main' into pr473-fix
samanyugoyal2010 e4f2a06
fix(react-native): address PR review on iOS handle lifetime and plugin
samanyugoyal2010 81b5d44
fix(react-native): validate bridged numbers; support query embeddings
samanyugoyal2010 adddd19
fix(react-native): support `filter` in query; document credential exp…
samanyugoyal2010 75fedd4
fix(react-native): download xcframework at podspec eval; clamp Float …
samanyugoyal2010 f02b52b
fix(react-native): make close() nonblocking; reject non-string filter…
samanyugoyal2010 cf57678
fix(react-native): keep Moss.xcframework out of the published package
samanyugoyal2010 9686b64
feat(react-native): bridge the authenticator for short-lived tokens
samanyugoyal2010 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,3 +27,4 @@ skills-lock.json | |
|
|
||
| # Swift / SwiftPM build artifacts | ||
| .build/ | ||
| .smoke-expo-moss/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # React Native / Expo example (Moss) | ||
|
|
||
| Minimal usage sketch for [`@moss-dev/moss-react-native`](../../sdks/react-native/). | ||
|
|
||
| This is not a full Expo app (no `node_modules` committed). Scaffold your own with: | ||
|
|
||
| ```bash | ||
| npx create-expo-app moss-rn-demo | ||
| cd moss-rn-demo | ||
| npx expo install @moss-dev/moss-react-native | ||
| ``` | ||
|
|
||
| Add the plugin in `app.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "expo": { | ||
| "plugins": ["@moss-dev/moss-react-native"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Then: | ||
|
|
||
| ```bash | ||
| npx expo prebuild | ||
| npx expo run:ios | ||
| ``` | ||
|
|
||
| ## Example screen | ||
|
|
||
| ```tsx | ||
| import { useEffect, useState } from 'react'; | ||
| import { Button, ScrollView, Text, View } from 'react-native'; | ||
| import { MossClient, type SearchResult } from '@moss-dev/moss-react-native'; | ||
|
|
||
| // Development build only. `EXPO_PUBLIC_*` values are inlined into the shipped JS | ||
| // bundle, so a project key set this way is readable by anyone with the app and | ||
| // grants mutating access (createIndex / addDocs / deleteIndex). Do not ship it | ||
| // in a production app — see the package README's "Credentials" section. | ||
| const PROJECT_ID = process.env.EXPO_PUBLIC_MOSS_PROJECT_ID!; | ||
| const PROJECT_KEY = process.env.EXPO_PUBLIC_MOSS_PROJECT_KEY!; | ||
|
|
||
| export default function App() { | ||
| const [result, setResult] = useState<SearchResult | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const client = new MossClient(PROJECT_ID, PROJECT_KEY); | ||
| let cancelled = false; | ||
|
|
||
| (async () => { | ||
| try { | ||
| await client.createIndex('rn-demo', [ | ||
| { id: '1', text: 'Refunds are processed within 3-5 business days.' }, | ||
| { id: '2', text: 'Shipping usually takes 2 business days.' }, | ||
| ]); | ||
| await client.loadIndex('rn-demo'); | ||
| const search = await client.query('rn-demo', 'how long do refunds take?'); | ||
| if (!cancelled) setResult(search); | ||
| } catch (e) { | ||
| if (!cancelled) setError(e instanceof Error ? e.message : String(e)); | ||
| } finally { | ||
| client.close(); | ||
| } | ||
| })(); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, []); | ||
|
|
||
| return ( | ||
| <ScrollView contentContainerStyle={{ padding: 24, gap: 12 }}> | ||
| <Text style={{ fontSize: 22, fontWeight: '600' }}>Moss RN demo</Text> | ||
| {error ? <Text style={{ color: 'crimson' }}>{error}</Text> : null} | ||
| {result?.docs.map((doc) => ( | ||
| <View key={doc.id}> | ||
| <Text> | ||
| [{doc.score.toFixed(3)}] {doc.text} | ||
| </Text> | ||
| </View> | ||
| ))} | ||
| <Button title={`SDK ${MossClient.sdkVersion}`} onPress={() => {}} /> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - iOS only for on-device query today. Android throws until [#411](https://github.com/usemoss/moss/issues/411) lands. | ||
| - Requires a development build — Expo Go cannot load custom native modules. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| node_modules/ | ||
| build/ | ||
| .DS_Store | ||
| *.tgz | ||
| ios/Frameworks/ | ||
| ios/Moss.xcframework/ | ||
| *.xcframework/ | ||
| .expo/ | ||
| package-lock.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| node_modules/ | ||
| ios/Frameworks/ | ||
| ios/Moss.xcframework/ | ||
| *.tgz | ||
| .DS_Store | ||
| tsconfig.json | ||
| .gitignore |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| BSD 2-Clause License | ||
|
|
||
| Copyright (c) 2026, Moss Team | ||
| All rights reserved. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without | ||
| modification, are permitted provided that the following conditions are met: | ||
|
|
||
| 1. Redistributions of source code must retain the above copyright notice, this | ||
| list of conditions and the following disclaimer. | ||
|
|
||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||
| this list of conditions and the following disclaimer in the documentation | ||
| and/or other materials provided with the distribution. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| # @moss-dev/moss-react-native | ||
|
|
||
| React Native / Expo module for [Moss](https://github.com/usemoss/moss) — on-device semantic search. | ||
|
|
||
| Closes the gap described in [usemoss/moss#432](https://github.com/usemoss/moss/issues/432). | ||
|
|
||
| ## Status | ||
|
|
||
| | Platform | Support | | ||
| |----------|---------| | ||
| | **iOS** | Native via `Moss.xcframework` (same binary as the Swift SDK, release `v0.6.2`) | | ||
| | **Android** | Stub — throws until Android native builds land ([#411](https://github.com/usemoss/moss/issues/411)) | | ||
| | **Expo Go** | Not supported (custom native code; use a [dev client](https://docs.expo.dev/develop/development-builds/introduction/) / `expo prebuild`) | | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npx expo install @moss-dev/moss-react-native | ||
| ``` | ||
|
|
||
| Add the config plugin in `app.json` / `app.config.js`: | ||
|
|
||
| ```json | ||
| { | ||
| "expo": { | ||
| "plugins": ["@moss-dev/moss-react-native"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Then regenerate native projects: | ||
|
|
||
| ```bash | ||
| npx expo prebuild | ||
| npx pod-install | ||
| ``` | ||
|
|
||
| CocoaPods downloads `Moss.xcframework` during `pod install` (checksum-verified against the Swift SDK release). | ||
|
|
||
| ## Quick start | ||
|
|
||
| > [!WARNING] | ||
| > The snippet below reads the project key from `EXPO_PUBLIC_*`, which is fine | ||
| > for a local dev build but **must not ship in a production app**. See | ||
| > [Credentials](#credentials) before you release. | ||
|
|
||
| ```tsx | ||
| import { MossClient } from '@moss-dev/moss-react-native'; | ||
|
|
||
| // Development builds only — see the Credentials section below. | ||
| const client = new MossClient(process.env.EXPO_PUBLIC_MOSS_PROJECT_ID!, process.env.EXPO_PUBLIC_MOSS_PROJECT_KEY!); | ||
|
|
||
| await client.createIndex('support-docs', [ | ||
| { id: '1', text: 'Refunds are processed within 3-5 business days.' }, | ||
| { id: '2', text: 'You can track your order on the dashboard.' }, | ||
| ]); | ||
|
|
||
| await client.loadIndex('support-docs'); | ||
| const result = await client.query('support-docs', 'how long do refunds take?'); | ||
| for (const doc of result.docs) { | ||
| console.log(`[${doc.score.toFixed(3)}] ${doc.text}`); | ||
| } | ||
|
|
||
| client.close(); | ||
| ``` | ||
|
|
||
| ## Credentials | ||
|
|
||
| Anything in an `EXPO_PUBLIC_*` variable is **inlined into the JS bundle at build | ||
| time**. It is shipped to every user and can be read straight out of the app — | ||
| it is not a secret. A project key exposed that way is usable by anyone who | ||
| extracts it, and `MossClient` also exposes mutating calls (`createIndex`, | ||
| `addDocs`, `deleteIndex`), so a leaked key is not merely read access to your | ||
| project. | ||
|
|
||
| So: | ||
|
|
||
| - **Development / internal builds** — `EXPO_PUBLIC_MOSS_PROJECT_KEY` is fine. | ||
| - **Production apps** — do not embed a project key. Use the token form below. | ||
|
|
||
| ### Short-lived tokens (production) | ||
|
|
||
| Pass `getAuthToken` instead of a project key. It is called whenever the native | ||
| runtime needs a bearer token, so your backend mints a short-lived, scoped one | ||
| and nothing long-lived is ever in the bundle: | ||
|
|
||
| ```ts | ||
| const client = new MossClient({ | ||
| projectId: process.env.EXPO_PUBLIC_MOSS_PROJECT_ID!, | ||
| getAuthToken: async () => { | ||
| const res = await fetch('https://api.example.com/moss-token', { | ||
| headers: { Authorization: `Bearer ${await mySessionToken()}` }, | ||
| }); | ||
| const { token } = await res.json(); | ||
| return token; // raw token — do NOT prefix with "Bearer " | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| Notes: | ||
|
|
||
| - Return the **raw token**. The native side builds the | ||
| `Authorization: Bearer <token>` header itself. | ||
| - `getAuthToken` may be called from a background thread and more than once — | ||
| cache until expiry if the round trip is expensive. | ||
| - Throwing (or returning a non-string / empty string) fails the in-flight | ||
| request with your error rather than hanging it. | ||
| - `projectId` is not a secret; only the key is. | ||
|
|
||
| This is the same mechanism as the Swift SDK's `Authenticator`, wired through | ||
| `moss_client_new_with_authenticator` in the native ABI. | ||
|
|
||
| ## API | ||
|
|
||
| Mirrors the Node `@moss-dev/moss` client for the core cloud + local query loop: | ||
|
|
||
| - `new MossClient(projectId, projectKey)` (development builds) | ||
| - `new MossClient({ projectId, getAuthToken, baseUrl? })` (short-lived tokens; see [Credentials](#credentials)) | ||
| - `createIndex(name, docs, options?)` | ||
| - `addDocs(name, docs, options?)` | ||
| - `loadIndex(name, options?)` / `unloadIndex(name)` | ||
| - `query(name, query, options?)` | ||
| - `listIndexes()` / `getIndex(name)` / `deleteIndex(name)` | ||
| - `close()` | ||
| - `MossClient.sdkVersion` | ||
| - `MossClient.setModelCacheDir(path)` (optional; iOS defaults to `Library/Caches/moss-models`) | ||
|
|
||
| The Swift SDK's `Authenticator` is bridged (see [Credentials](#credentials)). | ||
| Session APIs remain out of scope for this first release. | ||
|
|
||
| ### Metadata filters | ||
|
|
||
| `query` takes the same `filter` shape as `@moss-dev/moss`: | ||
|
|
||
| ```ts | ||
| await client.query('support-docs', 'refund timing', { | ||
| filter: { field: 'locale', condition: { $eq: 'en-US' } }, | ||
| }); | ||
| ``` | ||
|
|
||
| Pass `filterJson` instead if you already hold the engine's serialized form. | ||
| Supplying both is an error rather than a silent precedence rule. | ||
|
|
||
| ### Custom embeddings | ||
|
|
||
| Passing documents with an `embedding` makes `createIndex` select `modelId: 'custom'`, | ||
| which means there is no on-device model to embed query text with. Supply the query | ||
| vector yourself: | ||
|
|
||
| ```ts | ||
| await client.createIndex('vectors', [ | ||
| { id: '1', text: 'Refunds take 3-5 business days.', embedding: myVector }, | ||
| ]); | ||
| await client.loadIndex('vectors'); | ||
|
|
||
| const result = await client.query('vectors', 'refund timing', { | ||
| embedding: myQueryVector, // must match the index dimensionality | ||
| }); | ||
| ``` | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Expo SDK 54+ (or a React Native app with Expo Modules) | ||
| - iOS 16.4+ (Expo SDK 54+ baseline) | ||
| - Xcode 15+ | ||
| - Apple Silicon Mac for the iOS Simulator (the Moss.xcframework simulator slice is arm64-only) | ||
| - A development build / `expo prebuild` — Expo Go is not supported | ||
| - Until this package is published to npm, install from a local path or git checkout (`file:…` / `github:…`) | ||
|
|
||
| ## Development (this monorepo) | ||
|
|
||
| ```bash | ||
| cd sdks/react-native | ||
| npm install | ||
| npm run build | ||
| ``` | ||
|
|
||
| See [`examples/react-native/`](../../examples/react-native/) for a minimal usage sketch. | ||
|
|
||
| ## License | ||
|
|
||
| [BSD 2-Clause](./LICENSE) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.