A single document engineered to be pasted into Claude Code, Cursor, GitHub Copilot, or any LLM-powered coding assistant. Contains every API contract, security rule, and working code snippet a model needs to produce a correct Widget Builder integration on the first try.
Widget Builder is an embeddable widget authoring platform. Users in your host app pick templates, author HTML/CSS/JS widgets, save them to a per-tenant dashboard, and display them anywhere. It is NOT a CMS, JS sandbox, or standalone tool — it must be embedded. The tokens appSecret and embedToken must never leave the server. Use session-based auth via POST /api/embed/sessions minted from your backend.
POST /api/setupappId + appSecret to backend envGET /api/config on your host appPOST /api/embed/onboardembedToken in your DB keyed by tenantPOST /api/embed/sessionssessionId from your backend/embed/sdk.js scriptWidgetBuilder.init(...) or .addButton(...)x-app-secret on your API routesx-tenant-idwbs_... — long-lived app secret from the admin portal. Server-side only! Never expose.
emb_... — per-tenant token from onboarding. Server-side only!
ess_... — short-lived (24h) user session. Safe for the browser. Passed to the SDK.
Logical grouping. Symmetric: save with X → read with X. Auto-refresh bridge when same ID used on button + init.
Each onboarded org is an isolated tenant with its own widgets. Pass tenantId to every SDK method.
HTML + CSS + JS strings stored per tenant. Rendered in a sandboxed iframe. Fetches data via the widget-builder proxy.
Host Application (your product)
│
├── Backend (Node / Python / any)
│ │
│ ├── POST /api/embed/onboard → one-time per tenant (uses appId + appSecret)
│ │ returns: embedToken, organizationId
│ │
│ ├── POST /api/embed/sessions → per page load (appId + appSecret + embedToken)
│ │ returns: sessionId (ess_...)
│ │
│ └── Stores server-side ONLY: appId, appSecret, embedToken
│
└── Frontend (React / Vue / any)
│
├── Fetches sessionId from YOUR backend
├── Loads https://widget-builder.example.com/embed/sdk.js
└── Calls WidgetBuilder.init({ sessionId, ... })
↓
Widget Builder iframe loads the dashboard
↓
Widget inside iframe: fetchAPI('/api/projects')
↓
Widget Builder proxy: GET /api/embed/proxy/projects
↓
YOUR backend /api/projects receives:
x-app-secret: wbs_... ← validate!
x-tenant-id: your_tenant ← scope data by this!# Public — safe for the browser
NEXT_PUBLIC_WIDGET_BUILDER_URL=https://widget-builder.example.com
# Server-only — NEVER expose to the browser
WIDGET_BUILDER_URL=https://widget-builder.example.com
WIDGET_BUILDER_APP_ID=6651b...
WIDGET_BUILDER_APP_SECRET=wbs_...Core endpoints needed for a full integration. See the downloaded guide for request/response shapes and every endpoint in the API.
/api/setupBootstrapCreate the very first admin. One-shot (refuses to run twice).
/api/embed/onboardOne-time per tenantCreate org + owner user + embed token atomically. One call per tenant (not idempotent!).
/api/embed/sessionsPer page loadMint a 24h session for a user. Call from YOUR backend — never the browser.
/api/embed/usersAs neededAdd an additional user to a tenant. Call when /sessions returns 404 for a new user.
/api/embed/users/emailAs neededChange an existing user's email. Call after your own backend commits the new address.
/api/embed/sessions/{id}/refreshBefore expiryExtend a session TTL by 24 hours.
/api/embed/verifyHealth checkCheck if a session is still valid.
/api/embed/widgetsWidget CRUDList widgets. Headers: x-session-id, x-dashboard-id.
/api/embed/widgetsCreate a widget with html/css/js strings.
/api/embed/apisDiscoveryReturns the host app's API catalog (from /api/config) for editor autocomplete + AI.
import { NextRequest, NextResponse } from 'next/server'
import { getAuthenticatedUser } from '@/lib/auth'
const WB_URL = process.env.WIDGET_BUILDER_URL!
const APP_ID = process.env.WIDGET_BUILDER_APP_ID!
const APP_SECRET = process.env.WIDGET_BUILDER_APP_SECRET!
export async function POST(req: NextRequest) {
const user = await getAuthenticatedUser(req)
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 1. Resolve the per-tenant embed token (or onboard if missing)
let embedToken = await db.embedTokens.findOne({ tenantId: user.tenantId })
if (!embedToken) {
const onboardRes = await fetch(`${WB_URL}/api/embed/onboard`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-app-secret': APP_SECRET },
body: JSON.stringify({
organizationName: user.tenantName,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
appId: APP_ID,
}),
})
const { data } = await onboardRes.json()
await db.embedTokens.insertOne({
tenantId: user.tenantId,
token: data.embedToken.token,
})
embedToken = { token: data.embedToken.token }
}
// 2. Mint a session (handles the "user not found" 404 retry)
const sessionRes = await fetch(`${WB_URL}/api/embed/sessions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appId: APP_ID,
appSecret: APP_SECRET,
userEmail: user.email,
embedToken: embedToken.token,
}),
})
const session = await sessionRes.json()
return NextResponse.json({ data: session.data })
}'use client'
import { useEffect, useRef, useState } from 'react'
const WB_URL = process.env.NEXT_PUBLIC_WIDGET_BUILDER_URL!
export function WidgetBuilderDashboard({
dashboardId,
}: { dashboardId: string }) {
const [sessionId, setSessionId] = useState<string | null>(null)
const [sdkReady, setSdkReady] = useState(
typeof window !== 'undefined' && !!(window as any).WidgetBuilder,
)
const inited = useRef(false)
// 1. Fetch session from your backend
useEffect(() => {
fetch('/api/widget-builder-session', { method: 'POST' })
.then(r => r.json()).then(({ data }) => setSessionId(data.sessionId))
}, [])
// 2. Load sdk.js once (idempotent)
useEffect(() => {
if (sdkReady) return
const existing = document.querySelector<HTMLScriptElement>('script[data-wb-sdk]')
if (existing) {
existing.addEventListener('load', () => setSdkReady(true))
return
}
const s = document.createElement('script')
s.src = `${WB_URL}/embed/sdk.js`
s.async = true
s.dataset.wbSdk = 'true'
s.onload = () => setSdkReady(true)
document.head.appendChild(s)
}, [sdkReady])
// 3. Init dashboard + button when both ready
useEffect(() => {
if (!sessionId || !sdkReady || inited.current) return
inited.current = true
;(window as any).WidgetBuilder.init({
sessionId, baseUrl: WB_URL,
container: `#wb-${dashboardId}`,
dashboardId, height: '680px',
})
;(window as any).WidgetBuilder.addButton({
sessionId, baseUrl: WB_URL,
container: `#wb-btn-${dashboardId}`,
dashboardId, label: '+ Add Widget',
})
}, [sessionId, sdkReady, dashboardId])
if (!sessionId || !sdkReady) return <div>Loading…</div>
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: 8 }}>
<div id={`wb-btn-${dashboardId}`} />
</div>
<div id={`wb-${dashboardId}`} style={{ flex: 1 }} />
</div>
)
}Use it on multiple pages by varying dashboardId:
<WidgetBuilderDashboard dashboardId="projects" />
<WidgetBuilderDashboard dashboardId="staff" />
<WidgetBuilderDashboard dashboardId="reports" />If your app sends widget permissions
AI generation is permission-checked like any other authoring action: POST /api/embed/ai/widget/code requires create or an edit level other than NONE — someone who can only edit their own widgets keeps AI help, someone who can do neither is refused with a 403 before the model is called. Hide your addButton for roles without create using WidgetBuilder.getPermissions(), rather than letting them open a picker whose every outcome is refused. Apps that have not adopted RBAC are unrestricted and see none of this — see Widget Permissions (RBAC).
When widgets call fetchAPI('/api/projects'), it lands on YOUR /api/projects route via Widget Builder's proxy. Validate the app secret and scope by tenant:
export async function GET(request: Request) {
// 1. Authenticate — reject anything without your app secret
const appSecret = request.headers.get('x-app-secret')
if (appSecret !== process.env.WIDGET_BUILDER_APP_SECRET) {
return new Response('Unauthorized', { status: 401 })
}
// 2. Scope by tenant
const tenantId = request.headers.get('x-tenant-id')
const projects = await db.projects.find({ tenantId }).toArray()
return Response.json({ data: projects })
}appSecret and embedToken never appear in browser bundles, VITE_/NEXT_PUBLIC_ env vars, or git historysessionId (ess_...), 24h TTLx-app-secret validated on every host API route including /api/configx-tenant-id used to scope data in multi-tenant appsdashboardId passed to addButton + init so the auto-refresh bridge fires(tenantId, userId) — not re-minted every renderDownload the full markdown or copy its entire content to paste into Claude, Cursor, or any LLM-based coding tool.