@monoverse/voicebot-next
v0.8.0
Published
Next.js wrapper for the VoiceBot talking widget — drop the deployed pk_ embed onto a Next.js App Router app via next/script.
Downloads
46
Maintainers
Readme
@monoverse/voicebot-next
A tiny, production-grade Next.js wrapper for the VoiceBot talking widget. Drop a voice
(or chat) assistant onto your App Router app with one component and your public key, loaded the
Next way via next/script.
This package is a thin injector over the already-deployed VoiceBot browser embed. It loads
the CDN bundle, hands it your pk_ public key, and the deployed widget self-bootstraps:
exchanges the pk_ (+ the browser-set Origin) for an origin-locked session token and mounts,
grounded in your store's catalog. There is no widget code in this package and no merchant
backend is required.
Prefer a generic React/
useEffectinjector? Use@monoverse/voicebot-react— it also works in Next.js. This package exists to load vianext/script(Next dedupes the tag and sequences loading after hydration), which is the idiomatic App Router path.
Install
npm install @monoverse/voicebot-nextnext (^14 or ^15), react, and react-dom are peer dependencies.
Usage (App Router)
Render <VoiceBotWidget> once in your root layout so it persists across client navigations
and never re-injects per route. The component is 'use client'; importing it into a Server
Component layout is fine.
// app/layout.tsx (a Server Component — VoiceBotWidget is a Client Component)
import { VoiceBotWidget } from '@monoverse/voicebot-next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="uk">
<body>
{children}
<VoiceBotWidget publicKey="pk_live_xxx" />
</body>
</html>
);
}Voice or chat
<VoiceBotWidget publicKey="pk_live_xxx" variant="voice" /> {/* default */}
<VoiceBotWidget publicKey="pk_live_xxx" variant="chat" />voice loads widget-voice.js; chat loads widget-chat.js.
Props
| Prop | Type | Default | Description |
| ----------- | -------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
| publicKey | string (required) | — | Your pk_ publishable key. |
| variant | 'voice' \| 'chat' | 'voice' | Which widget bundle to load. |
| scriptSrc | string | https://api.monoverse.tech/widget | Override the CDN base (self-hosted API). Also sets data-api-base.|
| onError | (error: Error) => void | — | Called on script load failure or an empty publicKey. |
Reactivity
The VoiceBot widget core is a one-per-page singleton — one mounted instance owns a single session token, WebSocket, and microphone. The wrappers document one consistent model:
- This package (Next) is one-shot:
next/scriptcaches the<script>bysrc, so changing a prop does not re-inject. Place it once in the root layout (it never unmounts); to switch the key at runtime, remount the host (a Reactkey=). See the teardown caveat below. - The Vue wrapper is likewise one-shot (reads props at mount). The React wrapper instead
re-injects when
publicKey,variant, orscriptSrcchange.
SSR safety
next/script with strategy="afterInteractive" loads only in the browser after hydration, so
the widget code (which touches window/document) never runs on the server — the component is
SSR-safe by construction. On an empty publicKey it renders nothing and calls onError.
Teardown caveat (why root-layout placement matters)
next/script caches the <script> by src and intentionally does not remove it on unmount.
When this component unmounts it tears down the widget instance (window.VoiceBot?.destroy?.()
— closing the WebSocket and releasing the mic), but the cached <script> tag stays. Because the
script is cached, re-mounting on a later route may not re-run the bundle. Place the widget once
in the root layout (it never unmounts) to avoid this. If you must mount it on specific routes
with full re-init, use @monoverse/voicebot-react's useEffect injector instead, which removes
the tag on cleanup.
Provisioning your pk_
pk_ is issued server-side by a VoiceBot operator (a repo script — seed_public_key.py).
When the key is issued, you list the exact origins allowed to use it:
- List every origin explicitly.
https://shop.example,https://www.shop.example, andhttps://app.shop.exampleare three different origins. wwwand subdomains are not inherited — register each separately.- An origin is
scheme://host[:port].http≠https; a non-default dev port is part of the origin (e.g.http://localhost:3000). Register your dev origins too. - Wildcards are not supported.
If the widget does not appear, the current origin is almost always missing from the key's
allow-list — the token mint returns 403 and the widget silently does not mount (your page is
never broken). See the provisioning guide.
Security
pk_ is a public, origin-locked key — exactly like a Stripe publishable key. Shipping it in
your client bundle is safe and intended:
- The mint endpoint issues a token only if the browser-set
Originis in the key's allow-list; another site copying yourpk_gets a403. tenant_idis resolved server-side from thepk_and signed into the token claim — the client never sends it, so cross-tenant access is structurally impossible.- The WebSocket upgrade re-checks
Originagainst the token claim and closes (4403) on mismatch.
The paired secret sk_ (catalog/order sync HMAC) is never in the browser — that is a
separate server-to-server flow, unrelated to this package.
Scope: what works now vs. the next tier
Works now with this package — a consultant grounded in your catalog: it answers product questions, recommends items, and describes your shipping and payment options from your store data.
Next tier (not in this package) — first-party actions: routing add_to_cart,
navigation, or other writes into your own app state. That needs the handler-registration SDK
(createWidget + merchant executors, ADR-053 / T5), which is a separate, heavier package and is
out of scope here. This wrapper is a script injector, not an action bridge.
License
MIT
