@everfur/sdk (React Native and the web)
Drop Everfur's AI vet assistant and pet-health features (records, gait/video, photo checkup, pet-state media) into a React Native app, and the assistant into a website. What each of your customers and users can see is decided server-side by Everfur's entitlement control plane and rendered by the SDK, so you gate features without shipping an app update.
- Hexagonal, framework-free core with a thin RN layer; every surface settles (no thrown domain errors).
- Server-authoritative entitlements: a feature you are not entitled to renders a deliberate off-state, never a blank screen, and cannot be force-enabled client-side.
- The app never holds a secret. See The security model.
This branch prepares a future release under the Everfur SDK License. Requires React Native
>=0.74(or React>=18withreact-domfor the web entry), and Node>=18on the backend that mints sessions.
As of October 23, 2026, published SDK 0.14.0 and CLI/MCP 0.8.0 still contain MIT notices. Publishing
these docs does not replace those artifacts. See the license rollout status.
The version 2.1 acceptance flow is not yet available. Versions released under it will require acceptance
in the partner console, Sandbox credentials for evaluation, and a written Partner Agreement for production.
Those packages will include LICENSE.md, the full terms in SDK-LICENSE.txt, and THIRD_PARTY_NOTICES
for bundled open-source components. Those components retain their own licenses.
Getting keys: open the partner console and sign in. Where Everfur has enabled sandbox sign-up, create a sandbox (test-mode) tenant. If sign-up is unavailable, ask your Everfur contact to create one. Live tenants and their keys are provisioned by the Everfur team. Contact [email protected] to get set up. The publishable key is designed to ship inside your app; the secret key is not.
Install
npm i @everfur/sdk
# A React Native app installs these itself (react-native and react-native-safe-area-context are declared
# optional peers, so a web project is not handed them):
npm i react react-native react-native-safe-area-context
Peer dependencies by capability (install only what you use):
| Capability | Extra peer deps | Notes |
|---|---|---|
| chat, entitlements | none | works with react + react-native alone |
| records | Host-supplied document picker and upload transport | The SDK does not import Expo file-system or image-manipulator modules. |
| video (gait), photo (checkup) | react-native-vision-camera | use 4.x on RN 0.74; 5.x requires the new architecture |
| televet in-app call | @daily-co/react-native-daily-js and @daily-co/react-native-webrtc, plus Daily's own peers | tested pair: daily-js ^0.88.0 with webrtc 124.0.6-daily.2 (exact); the peer ranges also admit daily-js 0.90 with webrtc 155; iOS needs UIBackgroundModes audio; Expo SDK 55 to 57 use @daily-co/[email protected] |
The camera peer is optional: if you never mount ./video or ./photo, you do not need it.
EverfurRecords accepts the host's picker and upload transport, so use the native modules that fit your
app (including Expo SDK 57) without an SDK peer-dependency constraint. react-native-vision-camera needs a
camera usage description; 4.x also ships an Expo config plugin (add it to app.json plugins), while 5.x
ships none and instead needs its own peers react-native-nitro-modules and react-native-nitro-image.
Bundler resolution
No Metro configuration is required. Every subpath (@everfur/sdk/chat, /records, /televet, /video, /photo,
/animations, /client, /core, /testing, /testing/rn, /testing/web) resolves on a stock install. Modern bundlers
read the package exports map; Metro 0.80.x, which ships with Expo 51, defaults to
unstable_enablePackageExports: false and never reads it, so the package also ships classic root-level
resolution shims for the same subpaths. You do not need to turn package exports on, and turning them on does
not change what you get.
@everfur/sdk/server is the one exception, deliberately: it is your backend module and carries the
sk_partner_ session minter. If it is ever pulled into an app bundle it throws on import rather than
shipping the minter to a device. Import it from your server only.
On the web
Two ways, one wire contract, documented in docs/partner-integration/13-WEB-INTEGRATION.md:
- In-page, for a page whose React tree you own:
import { EverfurProvider, EverfurChat } from '@everfur/sdk/web'(peers:react,react-dom). The copy-paste shape isexamples/minimal-web.tsx. Take the/webentry, not the package root: the root is the React Native entry and importsreact-nativeandreact-native-safe-area-context, so in a browser project it fails to resolve them rather than rendering. The/webentries needreact,react/jsx-runtimeandzodand nothing native, whichnpm run verify:packproves on every CI run by importing every one of them in a React web project. - Frame mode, for a page you do not fully control (a storefront theme, a CMS page): one
<script src="https://sdk.everfur.com/v1/everfur.js">puts the surface in an iframe onsdk.everfur.com, and the frame refuses to initialise on any page origin your tenant has not registered. The copy-paste shape isexamples/frame-embed.html.
Chat and records are the web surfaces (records on its own subpath and its own frame document, available
when Everfur enables records for your account); photo and video remain React Native for now. Vet visits (available when Everfur enables them for your
account) have their own entry on both hosts, @everfur/sdk/web/televet and @everfur/sdk/televet, and a visit opens
INSIDE your app by default: booking, the confirmation and the call run in an Everfur modal (React Native) or
dialog (web) that VetVisitButton and EverfurChat mount themselves; the hosted page is an explicit opt-in,
prefer="redirect". The booking screens (@everfur/sdk/televet/booking, @everfur/sdk/web/televet/booking)
and the call (@everfur/sdk/televet/call, @everfur/sdk/web/televet/call) are their own entries. On the web they
are loaded on demand, so a page where nobody opens a visit downloads neither and a page where nobody joins takes
no WebRTC dependency; on React Native both are static imports in the same bundle (the WebRTC stack is the native
module your app registers with registerEverfurDaily). Because EverfurChat from @everfur/sdk/chat mounts the
visit host, a React Native app that imports it also bundles the booking and call screens; @everfur/sdk/chat/lite
is the same chat without that host, for an app that mounts <EverfurTelevetHost /> itself or opens no visits.
docs/partner-integration/16-TELEVET-VISITS.md.
Quickstart
Wrap your tree once, high, then drop features in behind a gate. This is the exact shape the reference app
uses, for a React Native host; a browser host imports the same names from @everfur/sdk/web (see On
the web):
import { EverfurProvider, EverfurChat, petRef, userRef } from '@everfur/sdk';
export default function App() {
return (
<EverfurProvider
config={{
publishableKey: process.env.EXPO_PUBLIC_EVERFUR_PUBLISHABLE_KEY!, // pk_test_... or pk_live_... safe to ship
apiBaseUrl: 'https://api.everfur.com/api/v1',
}}
// Omit `user` entirely for anonymous mode (publishable key alone, general answers).
user={{
userRef: userRef('your-stable-user-id'),
// An absolute URL: a relative path does not resolve in a React Native bundle.
// Your route answers JSON `{ token, expiresAt }`; see the contract below.
getToken: async () => {
const res = await fetch('https://your-backend.example.com/everfur/session', { method: 'POST' });
if (!res.ok) throw new Error('session mint failed: ' + res.status);
const { token, expiresAt } = (await res.json()) as { token: string; expiresAt: number };
return { token, expiresAt };
},
}}
activePet={petRef('pet-ref-123')}
>
{/* Every prebuilt widget self-gates. CapabilityGate is for YOUR surrounding UI - a tab, a nav
entry - not for wrapping a widget that already gates itself. */}
<EverfurChat petRef={petRef('pet-ref-123')} />
</EverfurProvider>
);
}
getToken calls your backend for a short-lived session token (never a secret in the app). Your route
answers JSON, { "token": sessionToken, "expiresAt": expiresAt } straight from mintPartnerSession, with a
non-2xx status on any failure, which is the shape every example in the docs uses:
import type { MintedToken } from '@everfur/sdk';
async function getToken(): Promise<MintedToken> {
const res = await fetch('https://your-backend.example.com/everfur/session', {
method: 'POST',
headers: { authorization: `Bearer ${yourAppsUserToken}` },
});
if (!res.ok) throw new Error('session mint failed: ' + res.status);
const { token, expiresAt } = (await res.json()) as { token: string; expiresAt: number };
return { token, expiresAt };
}
The SDK calls getToken lazily, re-mints about a minute before expiresAt (halfway through, for a token that
lives under two minutes), and re-calls it once after a
401, so a fresh token is always used without you managing expiry. Everfur never sees how you signed the person in (OAuth, a JWT, Firebase, Cognito, a
session, a magic link all work); it needs one stable, opaque, non-guessable id per person as userRef. Records lives on its own subpath:
import { EverfurRecords } from '@everfur/sdk/records';
import { petRef } from '@everfur/sdk';
<EverfurRecords petRef={petRef('pet-ref-123')} />
The security model
The one rule: the app never holds a secret. Two provisioning credentials, kept apart, plus a read-only
docs key for tooling. Test mode is a sandbox tenant (livemode: false in its entitlements), on staging or on
production; the API accepts a pk_test_ key only for a sandbox tenant, so a test key never reaches live data. A
sandbox key can start with pk_live_ or pk_test_, so read livemode, not the prefix. Each environment's API
base URL, keys and hosted MCP address are in
Environments and keys.
| Credential | Where it lives | What it does |
|---|---|---|
Publishable key pk_live_... or pk_test_... (a sandbox key can carry either) | In the app (EXPO_PUBLIC_...). Safe to ship. | Names your tenant. Cannot mint a session or grant a feature by itself. |
Partner secret key sk_partner_... | Only on your server. Never in the app, its bundle, env, git, or logs. | Mints session tokens. Revocable and rotatable without an app release. |
| Session token (bearer) | App memory, short-lived. | Authenticates one end user; re-minted via your backend on a 401. Signed, holds no secret, 15 minutes by default (1 minute to 1 hour). |
Docs key dk_partner_... | Only in the MCP server or CLI configuration on a developer machine. | Read-only: fetches the Everfur contract from GET /partners/contract and opens no other route. |
Your backend exchanges the two provisioning credentials for a short-lived session by calling
POST /widget/v1/sessions with x-everfur-partner-key: pk_... + x-everfur-partner-secret-key: sk_partner_..., both from the same tenant. The SDK ships a server-only helper (mintPartnerSession, imported from
@everfur/sdk/server) so you do not hand-roll that request. That helper is deliberately absent from every
React-Native-resolvable bundle: a device can never import it, so the secret has no path onto the phone.
Do: ship the publishable key (pk_test_ or pk_live_) in the app; keep sk_partner_ in a server secret manager; mint sessions on your
backend; serve everything over HTTPS.
Never: put sk_partner_ in the app; mint sessions on the device; try to force-enable a feature
client-side (the server re-checks and returns 403).
How feature access works
Access is decided server-side and delivered to the SDK (GET /widget/v1/entitlements, fail-closed). Each
surface sits behind CapabilityGate; a capability you are not entitled to renders a deliberate off-state,
and it cannot be force-enabled client-side because the API re-checks on every request. A coarse feature
(chat, records, video, ...) is enabled only when every dotted sub-feature resource it requires is
granted (for example records needs both records.upload.create and records.record.read). You set these
per customer and per user in Everfur's admin control plane; the app renders the verdict.
Every SDK surface renders all four states (loading, empty, error, populated); a denied surface renders its off-state, never a blank region.
Public entry points
| Import | Contents |
|---|---|
@everfur/sdk | EverfurProvider, CapabilityGate, EverfurChat, useEverfurChat, CapabilityName, error/result types, branded id helpers (petRef, userRef, conversationId) |
@everfur/sdk/records | EverfurRecords (the Everfur app's records flow), the named screens (EverfurRecordsUpload, EverfurRecordsRequests, EverfurRecordsRequest, EverfurRecordDetail, ...), EverfurClinicRequest, EverfurClinicRequestBatch, SignaturePad, records hooks + types |
@everfur/sdk/records/depth, @everfur/sdk/web/records/depth | record depth (the timeline, the withheld record sections, per-document contributions) on their own entries, dark until Everfur enables it per deployment; see chapter 9 |
@everfur/sdk/video | gait / live-scan surface |
@everfur/sdk/photo | photo checkup surface |
@everfur/sdk/animations | server-driven pet-state media surface |
@everfur/sdk/web | the browser entry: EverfurProvider, CapabilityGate, EverfurChat, useEverfurChat, EverfurTabs (Ask, Health Plan and Records in one shell, with the first-open intro), the theme hook, rendered with react-dom; withheld from React Native bundles the way ./server is |
@everfur/sdk/web/records | records for a browser host: EverfurRecordsPage (the Everfur web app's records page, complete on its own: request from a clinic, upload, request status, the full record with vaccinations, all inside your page), the named screens, EverfurRecords (the React Native-shaped flow), useEverfurRecords, createWebRecordsUploadTransport, fileToHandle; its own entry, withheld from React Native like ./web; available when Everfur enables records for the account. Frame mode serves it with surface: 'records' (see chapter 13) |
@everfur/sdk/web/records-sheet | recordsSheet(): the v3 records sheet and its lab result screen, EverfurRecordsPage's default look (loaded on first mount; sheet={false} keeps the classic page, which downloads none of it), or built with options and passed to its sheet prop; withheld from React Native like ./web; see chapter 13 |
@everfur/sdk/web/intro | the first-open intro EverfurTabs plays on a phone-narrow page (EverfurIntro, its clinic request frames, EverfurRecordsLandedBanner, useEverfurIntroSeen), downloaded only when it plays; withheld from React Native like ./web; see chapter 13 |
@everfur/sdk/web/records/upload | createWebRecordsUploadTransport alone, for the uploadTransport on the provider config at your app root; the same function @everfur/sdk/web/records exports, without the records entry |
@everfur/sdk/server | server-only mintPartnerSession, revokePartnerMemberSessions (end one member's sessions now) and the partner pet helpers (no React Native resolution; never bundled on-device) |
@everfur/sdk/server/events | server-only, Node only partner events (preview): constructEvent to verify webhooks, sendPartnerEvent; see chapter 15 |
@everfur/sdk/testing, @everfur/sdk/testing/rn, @everfur/sdk/testing/web | test seams (MockTransport, useEverfurTestEntitlements, ...) for your own tests |
@everfur/sdk/client and @everfur/sdk/core expose the framework-free layers for advanced integrators;
most apps only need the root entry plus the capability subpaths.
Reference app
A full, runnable integration (a fictional "Pawtrail" app plus a minimal mint backend that holds the secret)
lives at examples/minimal-integration.tsx in this repository; it is
type-checked by npm run docs:check, so it cannot rot. It shows anonymous mode, the two-credential session
mint, pet registration and sign-out. (A separate everfur-integration-example-rn app exists internally but
is not published anywhere a partner can reach, so do not rely on references to it.) It gates every
capability, and is the copy-paste source for the snippets above.