Skip to content

SDKs

Expo

On this page

@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.

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

  1. Import NotifiableProvider, useNotifiable, createNotifiable, and types from @notifiable/expo only. Do not import @notifiable/react-native in the app.
  2. Add the @notifiable/expo config plugin. Install expo-notifications, expo-constants, and AsyncStorage with npx expo install.
  3. Put the EAS project id in extra.eas.projectId and pass the same string as projectId.
  4. Mount one NotifiableProvider around the navigator. Pass publicKey, projectId, deepLinks, and onDeepLink. Never pass a push prop.
  5. Wait for ready before identify / inbox / track APIs. Call requestPermission() from a button.
  6. Treat "granted" as “Expo returned a token.” A system grant with no token stays "not_determined".
  7. In onDeepLink, navigate with the app’s router. The SDK already records delivery clicks for notification taps.
  8. 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.