farvex
v2.1.0
Published
Web SDK for Callpad voice sessions
Maintainers
Readme
farvex
Web SDK for Callpad voice sessions. It wraps the generated session HTTP client, Centrifugo realtime updates, a current browser session controller, React hooks, and LiveKit helpers.
Exports
| Entry | Purpose |
| ---------------- | ----------------------------------------- |
| farvex | Client factories and SDK/API types. |
| farvex/react | React provider and focused session hooks. |
| farvex/livekit | LiveKit re-exports and SessionRoom. |
Install
npm install farvex livekit-client @livekit/components-reactlivekit-client, @livekit/components-react, react, and react-dom are
peer dependencies. Install the LiveKit packages only in apps that render media.
Client Setup
import { createHostClient } from "farvex";
const client = createHostClient({
apiUrl: "https://api.callpad.example.com",
vendor: "acme",
userId: user.id,
auth: {
getAccessToken,
onUnauthorized: () => auth.signOut(),
},
});
await client.connect();
await client.sessions.sync(undefined, { mode: "replaceVisible" });getAccessToken is called before REST requests and realtime token refreshes.
SDK methods throw CallpadError for API problem responses.
API Shape
Use client.session for the current browser media session lifecycle:
const current = await client.session.start({
target: { type: "customer", userId: customer.id },
});
await client.session.end();Use client.sessions for the low-level collection/data API:
const sessions = await client.sessions.sync();
const session = client.sessions.get(current.sessionId);
const canRecord = session ? client.sessions.can(session, "startRecording") : false;The backend can support many live sessions. The browser SDK defaults to one
current media session per client instance. client.session.start() and
client.session.accept() use rejectWhileBusy by default, so starting or
accepting while a non-terminal current session exists fails locally with
session.busy. Pass { behavior: "allowParallel" } only when intentionally
creating and selecting another backend session.
StartedSession.join from the generated API is nullable. The high-level
client.session methods require a media grant by default and fail with
session.join_unavailable if the backend creates/accepts a session but does not
return a browser media grant.
React Usage
Import farvex/react from client components.
"use client";
import { useMemo, type ReactNode } from "react";
import { createHostClient, type HostClient } from "farvex";
import { CallpadProvider } from "farvex/react";
export const VoiceProvider = ({ children }: { children: ReactNode }) => {
const client = useMemo<HostClient>(
() =>
createHostClient({
apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
vendor: "acme",
userId: 42,
auth: { getAccessToken },
}),
[],
);
return (
<CallpadProvider client={client} connect disposeOnUnmount>
{children}
</CallpadProvider>
);
};Hooks:
| Hook | Purpose |
| ---------------------------- | ------------------------------------------- |
| useCallpad() | Current SDK client. |
| useStatus() | Realtime connection status. |
| useSessions() | Visible session collection. |
| useSession(id) | Exact session lookup. Pass null for none. |
| useCurrentSession() | Current browser media-session snapshot. |
| useCurrentSessionActions() | Current session commands and media events. |
| useIncomingInvites() | Pending invites for the current user. |
| useIncomingInvite() | First pending invite shortcut. |
| useCan(id, action) | UI helper for low-level session actions. |
| useSessionDuration(id) | Live elapsed time for an exact session ID. |
Starting a session:
import { useCallpad, useCurrentSession } from "farvex/react";
const StartButton = () => {
const client = useCallpad<HostClient>();
const { busy } = useCurrentSession();
const start = () =>
client.session.start({
target: { type: "phone", phone: "+15551234567" },
});
return (
<button type="button" disabled={busy} onClick={start}>
Start session
</button>
);
};Accepting an invite:
import { useCallpad, useIncomingInvite } from "farvex/react";
const IncomingInvite = () => {
const client = useCallpad<HostClient>();
const invite = useIncomingInvite();
if (!invite) {
return null;
}
return (
<>
<button
type="button"
onClick={() =>
client.session.accept({
sessionId: invite.session.id,
participantId: invite.participant.id,
})
}
>
Answer
</button>
<button
type="button"
onClick={() =>
client.sessions.reject({
sessionId: invite.session.id,
participantId: invite.participant.id,
})
}
>
Decline
</button>
</>
);
};Overlay and media should use the same current session ID:
import { RoomAudioRenderer, SessionRoom } from "farvex/livekit";
import { useCurrentSession, useCurrentSessionActions, useSession } from "farvex/react";
const SessionOverlay = () => {
const snapshot = useCurrentSession();
const actions = useCurrentSessionActions();
const session = useSession(snapshot.current?.sessionId ?? null);
return (
<>
<SessionRoom session={snapshot.current} controls={actions}>
<RoomAudioRenderer />
</SessionRoom>
{snapshot.current && session ? (
<SessionPanel session={session} phase={snapshot.current.phase} />
) : null}
</>
);
};LiveKit
farvex/livekit re-exports first-party LiveKit primitives and adds
SessionRoom. Use SessionRoom for the normal SDK flow; use raw LiveKitRoom
only for advanced media ownership.
<SessionRoom session={current} controls={client.session} audio video={false}>
<RoomAudioRenderer />
</SessionRoom>SessionRoom renders nothing without a current session or join grant. It passes
session.join.url and session.join.token to LiveKit and reports connected,
disconnected, and error events back to the session controller when controls is
provided.
Ringing Timeout
The SDK may hide expired incoming invites locally for immediate UX, but backend
REST/realtime state is authoritative for final session state. Apps should render
terminal state from VoiceSession.state, realtime events, and REST
reconciliation rather than treating a local timer as final.
Migration Notes
Replace no-ID useSession():
const { current } = useCurrentSession();
const session = useSession(current?.sessionId ?? null);Replace local raw join-grant state with client.session and SessionRoom.
The current session stores { sessionId, participantId, join }, so the overlay,
media room, controls, and participant UI all target the same session.
SDK source should use the exported Nullable<T> alias for nullable types instead
of direct null unions in public SDK types.
Development
From websdk/:
bun run generate
bun run check
bun run buildHTTP calls must go through the generated Hey API client in src/api/generated/.
Do not hand-edit generated files.
Release
Changesets lives in websdk/.changeset. From websdk/:
bun run changeset
bun run version
bun run release