SDKs
Expo

@notifiable/expo registers an Expo app with Notifiable: connect with a public key, ask for notification permission, save the Expo push token, identify the user, list their inbox, record opens and taps, and track events.
Import only from @notifiable/expo. The package owns expo-notifications. There is no push prop.
Install
npx expo install @notifiable/expo expo-notifications expo-constants @react-native-async-storage/async-storage
Peers: react ≥ 18, react-native ≥ 0.73, inside an Expo app. AsyncStorage keeps the subscriber between launches. Optional: expo-device, expo-application, expo-clipboard (better development sheet).
Add the config plugin, then create a new development build (plugin changes need a prebuild):
{
"expo": {
"plugins": ["@notifiable/expo"],
"extra": {
"eas": {
"projectId": "your-eas-project-id"
}
}
}
}
The plugin applies expo-notifications and excludes @notifiable/react-native native modules from the Expo build. If react-native.config.js already exists, exclude @notifiable/react-native yourself.
Integrate
Wrap the app in NotifiableProvider. It connects on mount. Permission is asked when you call requestPermission(), or when the dashboard requests the prompt.
import { NotifiableProvider, useNotifiable } from "@notifiable/expo";
function App() {
return (
<NotifiableProvider
publicKey="ntf_pub_..."
projectId="your-eas-project-id"
deepLinks={[
{ path: "/orders", label: "Orders" },
{ path: "/inbox", label: "Inbox" },
]}
onDeepLink={(url) => {
// Open this route in your navigator.
}}
>
<Home />
</NotifiableProvider>
);
}
| Prop | Required | Description |
|---|---|---|
publicKey |
yes* | Key from the Notifiable dashboard. Development key while building; production key for the store build. |
projectId |
recommended | EAS project id (extra.eas.projectId). Sent to getExpoPushTokenAsync. |
deepLinks |
no | Routes Notifiable can link to: { path, label }. |
onDeepLink |
no | Called with a notification url on tap (including cold start). |
storage |
no | Custom { getItem, setItem, removeItem }. Defaults to AsyncStorage. |
logger |
no | debug / info / warn / error callbacks. |
client |
no | Existing client from createNotifiable. Ignores publicKey / projectId. |
*Required unless you pass client.
Project id and tokens
A real Expo push token needs a development or production build and an EAS project id. Expo Go can grant permission and still return no token — connect still succeeds; the subscriber is saved without provider / token.
Pass the same string as extra.eas.projectId and as the projectId prop.
Deep links (Expo Router)
onDeepLink={(url) => {
const path = new URL(url, "https://notifiable.invalid").pathname;
router.push(path);
}}
Permission
function EnableNotifications() {
const { requestPermission } = useNotifiable();
return (
<Button title="Enable notifications" onPress={() => void requestPermission()} />
);
}
permission is "granted" only when Expo returns a push token. After a denial, a later requestPermission() opens Settings. An explicit requestPermission() that gets OS grant but no Expo token throws missing_token.
Notification taps
The provider shows foreground alerts and listens for taps (including the response that launched the app). On tap it reads data.url and a delivery id, calls clickDelivery when present, then onDeepLink when a url is present.
useNotifiable
Call under NotifiableProvider. Throws missing_provider outside that tree. Wait until ready before identify / inbox / track APIs.
const notifiable = useNotifiable();
if (!notifiable.ready) return <Text>Connecting…</Text>;
State
| Field | Meaning |
|---|---|
ready |
Connect finished and the subscriber was saved. |
permission |
"granted" only when Expo returned a token; else "denied" / "not_determined". |
subscriber |
Device/person record, or null until connect. provider is "expo" after a token is saved. |
clientConfig |
Dashboard app record, or null. |
deepLinks |
Saved routes, or null. |
devIndicator / diagnostics |
Development mark and crash-sharing choices. |
error |
Last NotifiableError, or null. |
config |
Options used to create this client (config.push.provider is "expo"). |
Methods
start() — Connects. Provider calls this on mount.
requestPermission() — System prompt, then save. Returns the new permission.
identify(input) — Attach a signed-in user (email and/or externalId).
await notifiable.identify({
email: "[email protected]",
externalId: "user_123",
firstName: "Ada",
lastName: "Lovelace",
});
inbox(query?) — Page of notifications (20 per page). Optional { page, opened }.
notification(id) / openNotification(id) / clickDelivery(deliveryId) — Load, mark opened, record tap.
track(name, properties?) — Event for the connected subscriber (≤20 keys; string / number / boolean / null).
await notifiable.track("order.shipped", { order: "1001", express: true });
setDevIndicator(enabled) / setDiagnostics(enabled) — Development mark and diagnostics. No-ops for a production key.
createNotifiable
import { createNotifiable, NotifiableProvider } from "@notifiable/expo";
const client = createNotifiable({
publicKey: "ntf_pub_...",
projectId: "your-eas-project-id",
deepLinks: [{ path: "/orders", label: "Orders" }],
onDeepLink: (url) => { /* navigate */ },
});
Always attaches the Expo push and device ports. Pass the client to NotifiableProvider so listeners and start() still run.
Development vs production
| Development key | Production key | |
|---|---|---|
| First launch | Sheet to show/hide the Notifiable mark | No sheet |
| On screen | Draggable mark (bundle id / package check) | Nothing overlaid |
| Notifications | Dashboard sends appear as top toasts while foreground + granted | Expo displays them |
| Diagnostics | Failed SDK calls can be shared | Nothing sent |
⌘N is not available in Expo builds (the React Native native shortcut module is excluded).
Errors
| Code | Meaning |
|---|---|
invalid_config |
Blank public key, or not iOS/Android. |
missing_provider |
useNotifiable() outside the provider. |
invalid_public_key |
Key rejected. |
network |
Request failed. |
validation_failed |
Rejected value. |
subscriber_not_found |
Connect not finished, or subscriber gone. |
subscriber_limit |
Project cannot add another subscriber. |
rate_limited |
Too many requests. |
missing_token |
Granted OS permission but Expo returned no token (explicit request throws; connect still saves as not_determined). |
missing_storage |
No AsyncStorage and no storage prop. |
not_implemented |
Behavior not available in this SDK. |
For AI agents
Agents should read this page as Markdown: /expo.md. Start from /llms.txt or /ai-agents.md when the framework is unknown.
Rules
- Import
NotifiableProvider,useNotifiable,createNotifiable, and types from@notifiable/expoonly. Do not import@notifiable/react-nativein the app. - Add the
@notifiable/expoconfig plugin. Installexpo-notifications,expo-constants, and AsyncStorage withnpx expo install. - Put the EAS project id in
extra.eas.projectIdand pass the same string asprojectId. - Mount one
NotifiableProvideraround the navigator. PasspublicKey,projectId,deepLinks, andonDeepLink. Never pass apushprop. - Wait for
readybefore identify / inbox / track APIs. CallrequestPermission()from a button. - Treat
"granted"as “Expo returned a token.” A system grant with no token stays"not_determined". - In
onDeepLink, navigate with the app’s router. The SDK already records delivery clicks for notification taps. - Do not invent hosts, env names, or CORS settings. Use the development key locally and the production key for store builds.
Integration prompt
Copy this into a coding agent when wiring the SDK into an existing Expo app:
Integrate @notifiable/expo into this Expo project.
1. Run npx expo install @notifiable/expo expo-notifications expo-constants @react-native-async-storage/async-storage.
2. Add "@notifiable/expo" to expo.plugins in app.json / app.config. Ensure extra.eas.projectId is set.
3. Find the root layout (e.g. app/_layout.tsx). Wrap the navigator in NotifiableProvider from @notifiable/expo.
4. Pass publicKey from env (development key for debug, production key for release), projectId matching extra.eas.projectId, deepLinks for main screens, and onDeepLink that navigates with Expo Router or the app’s navigator.
5. Do not pass a push prop. Do not import @notifiable/react-native.
6. After sign-in, when ready is true, call identify({ email, externalId }).
7. Add an “Enable notifications” control that calls requestPermission().
8. If there is an inbox UI, use inbox(), openNotification(id), and clickDelivery(deliveryId) for custom list interactions. Notification taps from Expo are already handled by the provider.
9. Remind that a new development build / prebuild is needed after adding the plugin. Do not explain CORS, API hosts, or environments — only publicKey and projectId. Keep changes minimal and match existing patterns.
