Client recipes
Render the widget.
Each framework has its own pattern for getting the JWT from your server to the component. The widget itself is the same across all of them.
signOnPilotIdentity helper from the server-side lib/onpilot file. See Server recipes for the helper body.Next.js — App Router
Server component signs, client component renders
The cleanest pattern for App Router. Sign in a server file, pass the token through to a client component that renders the widget.
// app/onpilot-widget.tsx
"use client";
import { CopilotBubble } from "@onpilot/react";
export function OnPilotWidget({ identityToken }: { identityToken: string }) {
return <CopilotBubble identityToken={identityToken} />;
}// app/layout.tsx
import { signOnPilotIdentity } from "@/lib/onpilot";
import { getCurrentUser } from "@/lib/session";
import { OnPilotWidget } from "./onpilot-widget";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getCurrentUser();
const identityToken = user ? signOnPilotIdentity(user) : null;
return (
<html>
<body>
{children}
{identityToken && <OnPilotWidget identityToken={identityToken} />}
</body>
</html>
);
}Next.js — Pages Router
Sign in getServerSideProps, pass as a prop
Pages Router doesn't have server components — sign per-request and pass the token through pageProps.
// pages/_app.tsx
import { CopilotBubble } from "@onpilot/react";
export default function App({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
{pageProps.onpilotIdentityToken && (
<CopilotBubble identityToken={pageProps.onpilotIdentityToken} />
)}
</>
);
}Remix
Sign in the loader, render in the route component
Loaders are server-only — perfect for signing the JWT. useLoaderData on the client component pulls it through.
// app/routes/_index.tsx
import { CopilotBubble } from "@onpilot/react";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { signOnPilotIdentity } from "~/lib/onpilot.server";
import { getUser } from "~/lib/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
const identityToken = user ? signOnPilotIdentity(user) : null;
return json({ identityToken });
}
export default function Index() {
const { identityToken } = useLoaderData<typeof loader>();
return (
<>
{/* your app */}
{identityToken && <CopilotBubble identityToken={identityToken} />}
</>
);
}Vite / CRA
Client-only app with a separate API
If your frontend is client-only, your backend (Node, PHP, Python, Ruby, Go — see Server recipes) signs a JWT and returns it from an authenticated endpoint. Fetch it on mount.
import { useEffect, useState } from "react";
import { CopilotBubble } from "@onpilot/react";
export function OnPilot() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
fetch("/api/onpilot/token", { credentials: "include" })
.then((r) => r.json())
.then((d) => setToken(d.identity_token));
}, []);
return token ? <CopilotBubble identityToken={token} /> : null;
}Or use identityTokenProvider to refresh automatically — see Identity tokens.
Angular
Angular — fetch the token, append the script
Angular apps are client-only after the build, so the identity token has to come from your backend at runtime. A tiny standalone component fetches your token endpoint on init, then appends the embed script with the token attached.
// src/app/onpilot-widget/onpilot-widget.component.ts
import { CommonModule, DOCUMENT } from "@angular/common";
import { HttpClient, HttpClientModule } from "@angular/common/http";
import { Component, Inject, OnInit } from "@angular/core";
@Component({
selector: "onpilot-widget",
standalone: true,
imports: [CommonModule, HttpClientModule],
template: "",
})
export class OnPilotWidgetComponent implements OnInit {
constructor(
private http: HttpClient,
@Inject(DOCUMENT) private document: Document,
) {}
ngOnInit(): void {
this.http
.get<{ identity_token?: string; chat_url?: string }>(
"/onpilot/token",
{ withCredentials: true },
)
.subscribe((res) => {
if (!res?.identity_token) return;
if (this.document.querySelector("script[data-onpilot-widget]")) return;
const script = this.document.createElement("script");
script.src = `${res.chat_url ?? "https://chat.onpilot.ai"}/embed.js`;
script.async = true;
script.dataset.identityToken = res.identity_token;
script.dataset.onpilotWidget = "true";
this.document.body.appendChild(script);
});
}
}Mount once in your root template:
<onpilot-widget></onpilot-widget>Plain HTML
Any server-rendered framework
Rails, Django, Laravel, Symfony, Express, plain PHP, WordPress — anything that can render a script tag. Your server signs the JWT, your template renders it into the data-identity-token attribute.
<script
src="https://chat.onpilot.ai/embed.js"
data-identity-token="<%= identityToken %>"
></script>That's the only client-side code. The script reads the token, exchanges it for a session, and mounts the widget. Widget style, theme, agent name, instructions, and suggestions all come from the dashboard.
Reach the iframe
Talk to the widget from outside the SDK
The widget renders inside an iframe with a stable title — `OnPilot Chat`. Use it as a selector when you need to push page context or UI signals into the widget from code that lives outside @onpilot/react (for example, a CRM record page where the widget is mounted by a parent layout you don't control).
// Tell the agent which record the user is currently viewing.
// The agent receives this as page_context on every submit, so it can
// answer "I see you're on Acme — Renewal Q3…" instead of a generic greeting.
document
.querySelector('iframe[title="OnPilot Chat"]')
?.contentWindow
?.postMessage(
{
type: "onpilot:context",
data: {
url: window.location.href,
title: document.title,
recordType: "deal",
recordId: "deal_8421",
recordName: "Acme — Renewal Q3",
recordData: { stage: "negotiation", amount: 48000 },
},
},
"*",
);The iframe only accepts messages after it posts onpilot:ready — buffer until then. The full event catalog (parent ↔ iframe, payloads, ready handshake) is on the postMessage protocol page.
Outbound API auth. The agent calls customer APIs via server-side connections — Composio OAuth (1000+ apps) or your uploaded OpenAPI spec's auth (Bearer / OAuth2). End-user JWTs are not forwarded from the host page to tool calls; per-end-user passthrough auth is on the roadmap, not the contract today.