vani / docs

Vani docs

Install

npm i @shvm/vani-client

Quick start (UI)

import { Vani } from "@shvm/vani-client/ui";

export function App() {
  return <Vani defaultMode="full" />;
}

Quick start (headless)

import { useVoiceSession } from "@shvm/vani-client/headless";

export function VoiceWidget() {
  const voice = useVoiceSession();
  return (
    <div>
      <div>Status: {voice.status}</div>
      <button onClick={voice.connect}>Connect</button>
      <button onClick={voice.cancel}>Cancel</button>
    </div>
  );
}

Server URL

useVoiceSession and <Vani /> default to https://shvm.in. Override with serverUrl if you’re running your own server.

Why Vani exists

  • Voice UX is brittle when state lives “somewhere else”. Durable Objects make a voice session a first-class, stateful primitive.
  • Keep the contract shared: the server and client speak the same typed websocket schema.
  • Reduce per-minute vendor lock-in by making “self-host” a normal path (while still letting you use managed providers when it’s the right call).

Cost notes (rough)

Vani is designed so the “expensive part” is the model usage, not your control plane. Running it on Cloudflare tends to mean you pay for Workers + Durable Objects + Workers AI usage. Exact cost depends on model choice, audio volume, and concurrency.

How it compares (high level)

  • Vapi: managed voice agent platform. Faster to ship, higher abstraction. Vani is lower-level and self-hostable by default.
  • LiveKit: real-time media infrastructure (WebRTC rooms, SFU). Great for calls/rooms and multi-party. Vani is a per-session voice agent runtime over a simple websocket + DO state.

These aren’t 1:1 competitors — the right pick depends on whether you need a managed voice platform, a media SFU, or a small self-hostable voice-agent runtime.

Self-host on your Cloudflare account

  1. Fork the repo and clone it locally.
  2. Use Node 22 (TanStack Start targets modern Node in CI).
  3. Install deps: npm ci
  4. Login to Cloudflare: npx wrangler login
  5. Deploy (site + voice server): npm run deploy

Make sure Workers AI is enabled on your Cloudflare account. The worker binds it as env.AI via "ai": { "binding": "AI" }.

The voice websocket route is /ws/:sessionId and is backed by a Durable Object named VOICE_SESSIONS configured in wrangler.jsonc.

// wrangler.jsonc (excerpt)
{
  "ai": { "binding": "AI" },
  "durable_objects": {
    "bindings": [
      { "name": "VOICE_SESSIONS", "class_name": "VoiceSessionDO" }
    ]
  },
  "migrations": [
    { "tag": "v2", "new_sqlite_classes": ["VoiceSessionDO"] }
  ]
}

The worker routes websocket upgrades in src/server.ts and forwards them to the per-session DO.

// src/server.ts (excerpt)
// Voice session websocket route
const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
if (wsMatch) {
  const sessionId = wsMatch[1];
  const id = env.VOICE_SESSIONS.idFromName(sessionId);
  const stub = env.VOICE_SESSIONS.get(id);
  return stub.fetch(request);
}

After deploying, point your own domain at the worker (Cloudflare dashboard → Workers & Pages → your worker → Triggers → Custom Domains). Then set serverUrl in your client/UI to your domain.

If you only want the voice server portion, you can keep the same Durable Object and websocket route and integrate the client package into a separate UI app — the contract is in @shvm/vani-client/shared.