@bubblio/server
v0.3.0
Published
Server SDK for Bubblio — drop a multi-provider, real-time customer-service AI character into any Node.js backend.
Maintainers
Readme
@bubblio/server
Server SDK for creating real-time AI character sessions with Bubblio — the SaaS layer for Runway avatars.
Drop into any Node.js backend. Two lines to create a session, signed HMAC callbacks for tool invocations, per-user context plumbing built in.
npm install @bubblio/serverQuickstart
Get a character ID and API key from bubblio.dev, then create a session endpoint in your app:
// app/api/bubblio/session/route.ts (Next.js App Router)
import { createBubblioSession, withUserContext, publicOrigin } from '@bubblio/server'
import { getCurrentUser } from '@/lib/auth'
export async function POST(req: Request) {
const user = await getCurrentUser()
const origin = publicOrigin(req)
const tools = await withUserContext(
{
secret: process.env.BUBBLIO_CALLBACK_JWT_SECRET!,
user: { id: user.id, email: user.email, name: user.name },
callbackUrl: `${origin}/api/bubblio/tools`,
},
[
{ name: 'get_orders', description: 'Get orders for the current user' },
{ name: 'cancel_order', description: 'Cancel an order by id' },
],
)
const session = await createBubblioSession({
bubblioApiKey: process.env.BUBBLIO_API_KEY!,
characterId: process.env.BUBBLIO_CHARACTER_ID!,
tools,
})
return Response.json(session) // { sessionId, sessionKey }
}That's the entire session-create side. The widget on the frontend fetches this endpoint and connects directly to Runway with the returned credentials.
Receiving tool callbacks
When the character calls a tool, Bubblio POSTs to your callbackUrl server-to-server. Verify the HMAC signature, extract the user context, run your logic, return JSON.
// app/api/bubblio/tools/route.ts
import { verifyWebhookSignature, userContextFromRequest } from '@bubblio/server'
import { db } from '@/lib/db'
type AppUser = { id: string; email: string; name: string }
export async function POST(req: Request) {
const raw = await req.text()
const sig = req.headers.get('x-bubblio-signature')
if (!verifyWebhookSignature(raw, sig, process.env.BUBBLIO_WEBHOOK_SECRET!)) {
return new Response('invalid signature', { status: 401 })
}
const user = await userContextFromRequest<AppUser>(
process.env.BUBBLIO_CALLBACK_JWT_SECRET!,
req,
)
const { tool, args } = JSON.parse(raw) as { tool: string; args: Record<string, unknown> }
if (tool === 'get_orders') return Response.json(await db.orders.byUser(user!.id))
if (tool === 'cancel_order') return Response.json(await db.orders.cancel(args.id as string, user!.id))
return new Response('unknown tool', { status: 400 })
}userContextFromRequest reads the ?ctx JWT that withUserContext stamped onto each callback URL at session create. The JWT is signed with your secret (BUBBLIO_CALLBACK_JWT_SECRET) — Bubblio never reads or decodes it, so user PII never touches the platform.
Deploying to Vercel — set SITE_URL
Vercel exposes two URLs to your function: VERCEL_URL (deployment-specific, e.g. myapp-kbcs0lqyh-team.vercel.app) and VERCEL_PROJECT_PRODUCTION_URL (the production alias). By default, deployment-specific URLs sit behind Vercel Deployment Protection — server-to-server calls from Bubblio hit a 401 wall and never reach your function.
Fix: set SITE_URL in your Vercel project env vars to your public production domain:
SITE_URL=https://your-site.compublicOrigin(request) resolves URLs in this order:
SITE_URL— explicit override (recommended)VERCEL_PROJECT_PRODUCTION_URL— production aliasVERCEL_URL— deployment URL (often gated)- The request's host header
If you see "errored" tool calls in the Bubblio dashboard with ~40-90ms latency but zero invocations in your function logs, this is what's happening.
API
createBubblioSession(config)
→ Promise<{ sessionId, sessionKey }>
// Platform mode requires bubblioApiKey + characterId (or personality).
// Self-hosted mode requires apiKey + avatarId (your own Runway account).
withUserContext({ secret, user, callbackUrl, expiresInSeconds? }, tools)
→ Promise<BubblioToolConfig[]>
// Stamps a per-session JWT onto every tool's callbackUrl as `?ctx=...`.
verifyWebhookSignature(rawBody, signatureHeader, secret)
→ boolean
// Constant-time HMAC-SHA256 check over the raw body.
// Always call before parsing JSON.
userContextFromRequest<T>(secret, request)
→ Promise<T | null>
// Reads `?ctx` from the request URL, verifies the JWT, returns the typed user.
// null = guest token; throws on missing/invalid.
signUserContext<T>(secret, user, expiresInSeconds?)
verifyUserContext<T>(secret, token)
// Lower-level JWT helpers (HS256). Use the helpers above unless you have a
// custom transport.
publicOrigin(request)
→ string
// Resolves the callback origin. See "Deploying to Vercel" above.Self-hosted mode
You can run the SDK against your own Runway account without the Bubblio platform. You lose hosted analytics, metering, and the dashboard — but it's there if you need it. Tools run inline as Node functions instead of HTTP callbacks:
const session = await createBubblioSession({
apiKey: process.env.RUNWAY_API_KEY!,
avatarId: process.env.RUNWAY_AVATAR_ID!,
personality: 'You are Aria, a helpful assistant…',
tools: [
{
name: 'get_orders',
description: 'Get orders for the current user',
handler: async () => ({ orders: await db.orders.all() }),
},
],
})Note: self-hosted mode keeps an open RPC connection per session, which doesn't work on Vercel / Cloudflare Workers / Lambda. Use platform mode there.
License
MIT. See LICENSE.
