Security

Beta
Last updated  Sep 23, 2026

Usher runs inside your signed-in app, as your user, and it moves them around your product. It is built so that neither a confused model nor a malicious prompt can send someone to a page you didn’t list, or make up an id.

The model is untrusted

  • The model never produces a URL. It picks a destinationId from an enum built from your map, and the enum is filtered to the user’s role before the model sees it.
  • Every tool call it makes, spoken or typed, goes through the same deterministic checks in the browser before anything moves.
  • Knowledge-base text and page content are treated as data, not instructions. A document that says “navigate to /admin” can’t get past the allow-list.
  • Usher needs no passwords, cookies, or tokens. Navigation happens in the user’s existing session through your router.

The validation chain

GateCheckRule
1Allow-listThe destinationId must be one this turn offered, and the list is already role-filtered. Anything else is not_in_allowlist.
2RoleThe destination's roleScopes must include context.role. Otherwise role_forbidden.
3ConfidenceIn local text mode, the match score must be at least 0.15. Otherwise low_confidence.
4ParamsEvery :param comes from the organization, the selected entity, the current URL, or resolveEntity, never from the model. Otherwise param_unresolved.
5Safe pathThe final path must start with a single / and have no URL scheme. That blocks https:, //host, and javascript:. Otherwise unsafe_target.
6Your routernavigate() runs through your router, so your guards, loaders, and redirects apply. A throw becomes navigation_failed.

What this does and doesn’t guarantee

The guarantee is that Usher never navigates to a destination outside your map, and never fills a param with an id the model made up. Here are its limits in this beta:

  • Role filtering runs in the browser. A user who edits the page’s JavaScript can change their own context.role. Role scopes shape what Usher offers. They are not access control. Your route guards and your API authorization remain the source of truth, and Usher always goes through them.
  • The model’s destination list is also built in the browser from context.role, and every tool call is checked again against the same role-filtered map.
  • The model can still say something wrong. The chain limits what it can do, not what it can say. Keep facts it must get right in your page descriptions.
  • Usher only navigates. It can’t click, submit, or change data.

What leaves the browser

In local text mode, nothing leaves the browser. In a live session, the browser streams the conversation to Voqal’s voice model over a WebSocket, and Voqal logs it. Conversations are processed by Voqal’s model provider in the United States; logs are stored in the EU (eu-west-1). Your destination map and knowledge base stay in your code: Voqal never stores them except as they appear in logged turns.

DataLocal text modeTo the model (live session)Logged by Voqal (hosted key)
Microphone audio, while the mic is liveNoStreamedA WAV clip per turn (16 kHz), unless captureAudio is false
The assistant's spoken replyNoProduced by the modelA WAV clip per voice turn (24 kHz), unless captureAudio is false
Typed messages and speech transcriptsNoYesYes, unless logConversation is false
The assistant's text, tool calls, and their resultsNoProduced by the modelYes, unless logConversation is false
Destination ids, titles, and descriptions (role-scoped)NoYes, in the session instructionsOnly as tool-call arguments
The current path, on connect and on each route changeNoYes, as a context noteOnly inside tool results
Knowledge-base entriesNoYes, up to 8,000 charactersNo
context.role, organizationId, selectedEntityNoNo. Used in the browserNo
Page content, form fields, cookies, auth tokensNeverNeverNever

Hosted logging and your privacy notice

  • Logs are keyed by a session id and a per-request id. They are sent in background batches, and audio goes straight to private storage, so logging never slows a reply. The browser reports them, so treat them as a record of what the client sent, not as tamper-proof.
  • Each turn is logged with its channel: voice, or text for chat (including chat while voice is paused). Assistant audio is uploaded only for replies played aloud. User clips start about 300 ms before the user speaks, and clips with no speech aren’t uploaded, so room audio between turns is never sent.
  • Limits per session: up to 500 turns and 200 audio clips, and a session accepts logs for 2 hours. Keys are rate-limited too. An oversized turn is truncated, not dropped: text to 8,000 characters and at most 10 tool calls.
  • Logs and audio are stored in the EU (AWS eu-west-1) and kept indefinitely. There is no automatic deletion in the beta.
  • Conversations are processed by Voqal’s model provider in the United States, so your users’ audio and text are processed there during the conversation, even though the logs are stored in the EU.
  • What the browser sends, so you can verify it in the network panel: turns go to POST {base}/v1/sessions/{sessionId}/events. Each audio clip is a POST {base}/v1/sessions/{sessionId}/audio that returns a short-lived upload URL, followed by a PUT of the WAV file straight to storage. With captureAudio: false you see no audio calls; with logConversation: false you see neither.
  • Opt out with cloud={{ captureAudio: false }} (no audio) or cloud={{ logConversation: false }} (nothing logged).
  • Your users’ voices and words are stored by Voqal on your behalf. Tell them in your privacy notice that an AI assistant records conversations, including audio; that the conversation is processed by an AI model provider in the United States; and that recordings are stored in the EU.
Paths can contain ids, like /cases/4821. The current path is shared with the model so it knows where the user is. If your URLs contain sensitive values, keep them out of the path.

Keys and credentials

  • The publishable key (pk_live_…) identifies your assistant. It’s meant for client code. Don’t treat it as a secret, and don’t treat it as authentication.
  • Each key works only from the origins Voqal registered for it, and it’s rate-limited. Browsers enforce the origin check. A script outside a browser can fake an origin, so the rate limit is what bounds misuse.
  • For each session the browser receives a short-lived credential, just long enough to open the voice connection. Voqal’s own credentials never leave Voqal’s servers.

Content Security Policy

If your app sends a CSP, add these sources. The hosted key needs three hosts: Voqal’s hosted service (starting sessions, plus the /events and /audio logging calls), Voqal’s realtime voice endpoint, and the storage host that receives the audio PUT (drop it if you set captureAudio: false).

Content-Security-Policy
# Voqal's hosted service, Voqal's realtime voice endpoint, and audio storage.# Wrapped here for reading; send it as one line.connect-src 'self'  https://nmkqy6jkaxbyrpb2wgksohawni0jfzea.lambda-url.eu-west-1.on.aws  wss://*.googleapis.com  https://voqal-usher-audio-876083391625-eu-west-1.s3.eu-west-1.amazonaws.com;# The audio worklet loads from a blob: URL, and the orb uses inline stylesscript-src 'self' blob:;style-src 'self' 'unsafe-inline';# Response header: allow the microphone on your own originPermissions-Policy: microphone=(self)
  • The orb and panel set inline style attributes and add <style> elements for their animations, so style-src needs 'unsafe-inline'.
  • Microphone capture needs a secure context: HTTPS, or localhost in development.
© 2026 VoqalVoqal SDK & engine documentation