@onpilot/node
v0.3.0
Published
Node.js SDK for OnPilot — tenant-key copilot resolution
Maintainers
Readme
@onpilot/node
Server-side SDK for embedding OnPilot copilots into your app.
The browser never sees your tenant secret. Your backend signs a short-lived
tenant identity JWT with embedSecret, names the copilotId it wants to
serve, and either hands the JWT to @onpilot/react or exchanges it for a
session token at POST /api/v1/embed/resolve.
Flat model — you own the mapping
OnPilot has no concept of "workspace" or "org" at the embed boundary. The
identity JWT names a single copilotId and that's what gets served.
If your product has many tenants/workspaces and you want each to see a different copilot, you keep the mapping:
your_app.workspace.id → onpilot.copilotIdLook it up per request, sign the token with that copilotId. Same contract
Botpress uses (their clientId == our copilotId). OnPilot just serves
whichever copilotId your backend puts in the JWT.
Install
npm install @onpilot/nodeConfigure
From the OnPilot dashboard, copy three values:
tenantId— your tenant UUID (Settings → Profile)embedSecret— the shared HS256 secret (Settings → Profile → Embed Secret)copilotId— the copilot's ID (Copilot page → Deploy)
Store the first two as server-side env vars. Store copilotId wherever makes
sense for your app (env var if you only have one copilot; your own DB if you
map per-tenant / per-workspace):
ONPILOT_TENANT_ID=...
ONPILOT_EMBED_SECRET=...
# Simple case — one copilot for the whole app:
ONPILOT_COPILOT_ID=...Quickstart — resolve a session for the current user
import { OnPilot } from "@onpilot/node";
const onpilot = new OnPilot({
tenantId: process.env.ONPILOT_TENANT_ID!,
embedSecret: process.env.ONPILOT_EMBED_SECRET!,
});
// Inside an authenticated route handler:
const session = await onpilot.resolve({
copilotId: process.env.ONPILOT_COPILOT_ID!, // or look up from your DB
user: {
id: req.user.id,
name: req.user.displayName,
email: req.user.email,
role: req.user.isAdmin ? "admin" : "user",
},
});
// session.sessionToken — short-lived (1h) session JWT, safe to expose
// session.chatUrl — chat iframe URL
// session.copilotId — echoed back
res.json(session);Sign without exchanging (hand the JWT to @onpilot/react)
If your frontend uses @onpilot/react, the React SDK does the resolve call
itself — you just hand it a signed identity JWT:
const identityToken = onpilot.signIdentityToken({
copilotId: process.env.ONPILOT_COPILOT_ID!,
user: {
id: req.user.id,
name: req.user.name,
email: req.user.email,
role: "admin",
},
expiresIn: "24h",
});
res.json({ identityToken });Then on the client:
import { CopilotBubble } from "@onpilot/react";
<CopilotBubble identityToken={identityToken} />Per-workspace copilots (advanced)
If different parts of your app should talk to different copilots, keep your own mapping and look it up per request:
// Your own table, populated during your onboarding:
// workspace_copilot_mapping(workspace_id PK, onpilot_copilot_id)
const mapping = await db.workspaceCopilotMapping.findUnique({
where: { workspaceId: req.user.workspaceId },
});
if (!mapping) {
return res.status(404).json({ error: "No copilot configured for this workspace" });
}
const identityToken = onpilot.signIdentityToken({
copilotId: mapping.onpilotCopilotId,
user: { id: req.user.id, email: req.user.email, role: "admin" },
});For now, each copilotId must be created manually in the OnPilot dashboard.
A public POST /api/v1/copilots for programmatic provisioning is on the
roadmap — see TODO_FUTURE_FEATURES.md (FF-001).
API
new OnPilot(options)
| Option | Type | Description |
| --- | --- | --- |
| tenantId | string | Tenant UUID — used as the JWT iss claim. Required. |
| embedSecret | string | Shared HS256 secret. Required. |
| baseUrl | string | Dashboard origin. Defaults to https://chat.onpilot.ai. |
| fetch | typeof fetch | Override for older Node or tests. |
onpilot.signIdentityToken(opts) → string
Sign and return a tenant identity JWT. No network call.
| Option | Type | Description |
| --- | --- | --- |
| copilotId | string | Target copilot ID. Required. |
| user.id | string | Authenticated user ID. Required. |
| user.name, user.email, user.role | string | Optional claims. |
| expiresIn | string \| number | JWT lifetime. Defaults to "24h". |
onpilot.resolve(opts) → Promise<ResolvedSession>
Signs an identity JWT and exchanges it at POST /api/v1/embed/resolve.
Takes the same arguments as signIdentityToken plus an optional
signal: AbortSignal.
Returns camelCase fields: copilotId, copilotName, userId, role,
sessionToken, chatUrl, expiresAt, expiresIn.
Migration from 0.1.x
0.2.x drops the externalOrgId parameter. Identity tokens now carry
copilotId directly — no server-side org resolution hop.
If you were calling:
onpilot.signIdentityToken({
externalOrgId: workspace.id,
user: { ... },
});Change to:
onpilot.signIdentityToken({
copilotId: await getCopilotIdForWorkspace(workspace.id),
user: { ... },
});where getCopilotIdForWorkspace is your own lookup.
License
MIT
