Skip to content

Commit 2a203a1

Browse files
committed
docs(web): secure transient assistants via proxy-injection; link from Web SDK and quickstart
1 parent 84361eb commit 2a203a1

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed

fern/quickstart/web.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ vapi listen --forward-to localhost:3000/webhook
4949

5050
## Web voice interfaces
5151

52+
<Info>
53+
Need to keep assistant config server-side? Use a proxy and `apiBaseUrl` to inject transient assistant settings securely: [Secure transient assistants on web](/sdk/web-secure-proxy)
54+
</Info>
55+
5256
Build browser-based voice assistants and widgets for real-time user interaction.
5357

5458
### Installation and setup

fern/sdk/web-secure-proxy.mdx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
title: Secure transient assistants on web
3+
subtitle: Start calls via a proxy without exposing assistant config
4+
slug: sdk/web-secure-proxy
5+
---
6+
7+
## Overview
8+
9+
Keep your assistant configuration on the server while using the Web SDK. This guide shows how to inject server‑owned transient assistant config at a proxy so the browser never sees secrets.
10+
11+
**In this guide, you'll learn to:**
12+
- Start web calls without exposing model/voice/system prompt
13+
- Use `apiBaseUrl` to route through your proxy
14+
- Harden your proxy for production
15+
16+
<Warning>Joining an already-created call by ID is not publicly documented in the Web SDK today. Use the proxy‑injection approach below, or confirm timing with support.</Warning>
17+
18+
## When to use this
19+
20+
- **Transient assistants**: You don’t want model, voice, or prompts in the browser
21+
- **Policy constraints**: You must keep secrets server-side
22+
- **Low latency**: Avoid creating a persistent assistant before each call
23+
24+
## Implementation
25+
26+
<Steps>
27+
<Step title="Set up a secure proxy">
28+
Implement a proxy that constructs the assistant payload server-side and authenticates with your server API key. Ignore any client-sent assistant config.
29+
30+
<CodeBlocks>
31+
```javascript title="Express proxy (Node.js)"
32+
import express from "express";
33+
import fetch from "node-fetch";
34+
import cors from "cors";
35+
36+
const app = express();
37+
app.use(express.json({ limit: "1mb" }));
38+
39+
// Allow only your frontend origin
40+
app.use(cors({ origin: "https://your-frontend.example.com", credentials: true }));
41+
42+
// Minimal app->proxy auth (recommended)
43+
app.use((req, res, next) => {
44+
const token = req.header("x-app-auth");
45+
if (token !== process.env.APP_PROXY_TOKEN) {
46+
return res.status(401).json({ error: "Unauthorized" });
47+
}
48+
next();
49+
});
50+
51+
// Only forward the Vapi paths you need
52+
const isAllowedVapiPath = (pathname) => pathname === "/call" || pathname.startsWith("/call/");
53+
54+
app.all("*", async (req, res) => {
55+
const url = new URL(req.url, "http://proxy.local");
56+
if (!isAllowedVapiPath(url.pathname)) {
57+
return res.status(404).json({ error: "Not found" });
58+
}
59+
60+
try {
61+
// Non-sensitive hints from client
62+
const clientMetadata = (req.body && req.body.metadata) || {};
63+
const scenario = clientMetadata.scenario || "default";
64+
65+
// Server-owned assistant config
66+
const assistantConfigByScenario = {
67+
default: {
68+
model: { provider: "openai", model: "gpt-4o-mini" },
69+
voice: { provider: "11labs", voiceId: "burt" },
70+
messages: [{ role: "system", content: "You are an assistant." }],
71+
},
72+
onboarding: {
73+
model: { provider: "openai", model: "gpt-4o" },
74+
voice: { provider: "11labs", voiceId: "rachel" },
75+
messages: [{ role: "system", content: "You help onboard new users." }],
76+
},
77+
};
78+
79+
const assistant = assistantConfigByScenario[scenario] || assistantConfigByScenario.default;
80+
81+
// Authoritative body (ignore any client-sent assistant config)
82+
const serverBody = {
83+
assistant,
84+
metadata: clientMetadata,
85+
};
86+
87+
const vapiResponse = await fetch(`https://api.vapi.ai${url.pathname}${url.search}` , {
88+
method: req.method,
89+
headers: {
90+
"content-type": "application/json",
91+
authorization: `Bearer ${process.env.VAPI_API_KEY}`,
92+
},
93+
body: req.method === "GET" || req.method === "HEAD" ? undefined : JSON.stringify(serverBody),
94+
});
95+
96+
const text = await vapiResponse.text();
97+
res.setHeader("content-type", vapiResponse.headers.get("content-type") || "application/json");
98+
res.status(vapiResponse.status).send(text);
99+
} catch (error) {
100+
console.error("Proxy error:", error);
101+
res.status(502).json({ error: "Upstream error" });
102+
}
103+
});
104+
105+
const port = process.env.PORT || 3001;
106+
app.listen(port, () => console.log(`Proxy listening on ${port}`));
107+
```
108+
</CodeBlocks>
109+
110+
<Tip>
111+
Do not forward client Authorization. Always set your server API key in the proxy.
112+
</Tip>
113+
</Step>
114+
115+
<Step title="Initialize the Web SDK against your proxy">
116+
Point the Web SDK at your proxy using `apiBaseUrl` and keep only the public key on the client.
117+
118+
<CodeBlocks>
119+
```typescript title="Frontend init (TypeScript)"
120+
import Vapi from "@vapi-ai/web";
121+
122+
const vapi = new Vapi(
123+
process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY!, // public key
124+
"https://your-proxy.example.com" // apiBaseUrl -> your proxy
125+
);
126+
```
127+
</CodeBlocks>
128+
</Step>
129+
130+
<Step title="Start a call without exposing config">
131+
Send only non-sensitive metadata from the client. The proxy injects model, voice, and system prompt.
132+
133+
<CodeBlocks>
134+
```typescript title="Start a call (Frontend)"
135+
await vapi.start({
136+
metadata: { scenario: "onboarding", userId: "abc_123" }
137+
});
138+
```
139+
</CodeBlocks>
140+
141+
<Check>Assistant configuration never leaves your server.</Check>
142+
</Step>
143+
144+
<Step title="Harden the proxy">
145+
- Replace Authorization with your server API key
146+
- Whitelist required Vapi paths only (e.g., `/call`)
147+
- Validate inputs and discard client-sent assistant config
148+
- Restrict CORS to your domain
149+
- Add rate limiting and minimal app auth (`x-app-auth`)
150+
- Avoid logging request bodies or secrets
151+
</Step>
152+
</Steps>
153+
154+
## Alternatives
155+
156+
- **Ephemeral assistantId**: Create a short-lived assistant server-side, then `vapi.start({ assistantId })`.
157+
- Pros: No config in browser. Cons: Extra API call; less “transient”.
158+
- **Backend-create-and-join (Daily-first)**: Create the call server-side and provide a Daily join token/URL to the client; wire UI/events with Daily JS.
159+
- Pros: Zero config in browser. Cons: More custom plumbing.
160+
161+
## Notes on join-by-ID
162+
163+
<Note>Joining an existing call by ID in the web SDK is not publicly documented at this time. If this becomes available, you can switch to a backend-create-then-join flow without the proxy injection.</Note>
164+
165+
## Reference
166+
167+
- Web SDK basics: [/sdk/web](/sdk/web)
168+
- Quickstart (web): [/quickstart/web](/quickstart/web)
169+
- API reference: [/fern/api-reference](/fern/api-reference)

fern/sdk/web.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ vapi.on("error", (e) => {
237237

238238
---
239239

240+
### Secure transient assistants (proxy)
241+
242+
Use a backend proxy to inject server-owned assistant config so the browser never sees secrets. Learn the recommended pattern: [Secure transient assistants on web](/sdk/web-secure-proxy)
243+
240244
## Resources
241245

242246
<CardGroup cols={2}>

0 commit comments

Comments
 (0)