A support inbox that lives inside the app
Mobile support has a shape web support does not. The question arrives from a specific build, on a specific OS version, from someone who may be on a train with two bars of signal, and who will not read a reply until they open the app again. A web chat widget in a WebView handles none of that well.
@offload/react-native is a headless Expo SDK: you mount it once, and open the support sheet from your own UI. The agent answers from your documentation, attaches its sources, and escalates to you when nothing grounds an answer — the same queue, the same confidence floor and the same knowledge base as the web widget and your support email.
Mount it once
The launcher is off by default: no floating bubble unless you ask for one. Your own Help button calls offload.open().
// app/_layout.tsx
import { Offload, offload } from '@offload/react-native';
export default function RootLayout() {
useEffect(() => { void offload.enableNotifications(); }, []);
return (
<>
<Stack />
<Offload
convexUrl={process.env.EXPO_PUBLIC_OFFLOAD_URL!}
publishableKey={process.env.EXPO_PUBLIC_OFFLOAD_KEY!}
user={{ id: user.id, email: user.email, name: user.name }}
/>
</>
);
}
Calls made before <Offload /> mounts are buffered and replayed in order, so a deep link from a push tap on cold start does not get lost.
Identity you can trust
Anyone can type someone else's email into a support chat. When identity verification is on for your workspace, every customer must carry a userHash — an HMAC-SHA256 of the customer id, computed on your backend with the identity secret from Settings. Never in the app: shipping the secret in a bundle defeats the point.
// at login — the hash comes from your own API
const { userHash } = await api.get('/support/identity');
offload.identify({ id: user.id, email: user.email }, { userHash });
await offload.logout(); // clears the customer, the hash and the sheet
Without a valid hash, conversation queries are rejected with IDENTITY_UNVERIFIED. The SDK logs a warning and shows empty states — it never silently mixes one customer's threads into another's session.
Push, and what happens offline
- Push on reply.
enableNotifications({ projectId })registers an Expo push token and notifies the customer when support answers, whether that answer came from the AI or from you. Tapping the notification deep-links straight into the thread. - Plays nicely with your own handler. Apps that already call
setNotificationHandlercan passmanageForegroundPresentation: false; the tap listener and token registration stay additive. - Optimistic echo. A message sent on a slow network appears in the thread immediately and reconciles when the server confirms it.
- Unread that survives a cold start. Read marks come from the server so they are shared across the customer's devices, with AsyncStorage as the offline fallback. Any storage adapter works — MMKV takes four lines.
- Badge without opening the sheet.
offload.onUnreadCount(setBadge), oruseOffload()if you prefer hooks.
Diagnostics the customer should not have to type
Every new message carries the app version and build, the OS and its version, the device model and the SDK version. The agent uses those facts to diagnose instead of asking for them, which is how "since the last update" becomes an answer rather than three round trips. No device identifier and no advertising identifier is collected.
If you turn the SDK off from the Offload dashboard, the launcher disappears and offload.open() becomes a no-op — a kill switch that does not need an app-store release.
The sheet itself
Native, not a WebView. Accent hero home, Conversations / Feature requests / Articles tabs, threaded bubbles, image attachments, resolution feedback and a matching composer — the same interior as the web widget, from the same copy dictionary, so wording is identical across both SDKs.
The shell speaks seven languages (English, French, Spanish, German, Italian, Portuguese, Dutch) and follows user.locale, then the device locale. Agent replies are translated server-side into whatever language the customer wrote in, and a language gate blocks any reply that came out in the wrong one.
Getting the package
@offload/react-native ships with your beta invitation, alongside the web widget and the browser client. React Native 0.73 and newer, React 18 or 19, with react-native-safe-area-context as the only required peer. expo-notifications, expo-image-picker, expo-application, expo-device and @react-native-async-storage/async-storage are optional — each one is only needed for the feature it powers.
Put it in your next build.
Join the waitlist