Skip to main content

Embed Nexatron in a React app

This quickstart drops a fully working Nexatron chat surface into an existing React app in about 15 minutes: 5 minutes for the SDK install, 5 minutes for the embed-token endpoint, 5 minutes for the styling pass to match your brand.

We use <NexatronChat />, the SDK's drop-in component, so you do not have to build a messages list, input, loading spinner, or error banner from scratch. If you want full control over the UI, drop down to the useNexatronChat hook (see Custom UI below).

1. Install the SDK (1 minute)​

Not on npm yet. @nexatron/chat is not published to the npm registry, so npm install @nexatron/chat fails today. Ask support for the release tarball and install that file; imports stay the same.

npm install ./nexatron-chat-0.8.0.tgz
# or
pnpm add ./nexatron-chat-0.8.0.tgz

The SDK supports React 18+. The package is split into three subpath exports:

EntryWhen to use it
@nexatron/chatHeadless NexatronClient for non-React apps, server code, or scripts.
@nexatron/chat/reactReact provider, hook, and <NexatronChat /> component.
@nexatron/chat/vanilla<nexatron-chat> Web Component for non-React contexts (Vue, Svelte, Astro, plain HTML).

2. Mint an embed token (5 minutes)​

The browser never sees a long-lived Nexatron credential. Instead, your backend mints a short-lived embed token that names the person the embed is for, and your React app forwards that token to the SDK.

Naming that person is the whole point. An embed token carries a viewer_user_id, and every request it authenticates runs as that viewer: their roles, their row-level security, their column masking. Two of your users embedding the same widget get two different answers to the same question, because Nexatron resolves each one's own access.

First, create the API key your backend will authenticate with. In Nexatron go to Settings → Integrations → API tokens → New token and tick "Mint embed tokens" (embed.mint). That scope is what lets a service credential call the mint endpoint. A key without it gets a 403, because API keys authenticate as api_consumer, and the endpoint's other branch matches a workspace-admin browser session alone. Only a workspace admin can grant the scope, and only a token that was deliberately granted it can mint.

Then add a tiny endpoint to your backend that proxies POST /api/v1/embed/tokens:

// pages/api/nexatron/token.ts (Next.js example)
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// 1) Authenticate the user with YOUR auth (session cookie, NextAuth, etc.)
const userId = await yourAuth(req);
if (!userId) return res.status(401).end();

// 2) Map that person to the Nexatron user they are. The mapping is yours to
// keep — Nexatron resolves governance from this id, so getting it wrong
// shows one user another user's rows.
const { nexatronUserId } = await yourTenantLookup(userId);

// 3) Mint the short-lived token. NEXATRON_API_KEY is an nxa_… key holding
// the `embed.mint` scope — see above.
const r = await fetch("https://api.nexatron.io/api/v1/embed/tokens", {
method: "POST",
headers: {
// Must be a tenant_admin credential — see the caution below.
"Authorization": `Bearer ${await yourNexatronAdminToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
// Required for the chat SDK. Without it the token can still render a
// shared dashboard snapshot, but it cannot authenticate an API call.
viewer_user_id: nexatronUserId,
// "query" is what the SDK needs. Add "dashboard" only if the same token
// also renders <NexatronDashboard />.
scopes: ["query"],
expires_in_hours: 1,
}),
});
if (!r.ok) return res.status(502).end();
const { token } = await r.json();
res.json({ token });
}

Why a short-lived embed token rather than a long-lived JWT in NEXT_PUBLIC_*: tokens minted server-side never reach the browser bundle, expire after at most an hour, and stop working the moment the viewer they name is disabled.

Minting requires a tenant_admin credential, not an API key

POST /api/v1/embed/tokens is role-gated to tenant_admin. A Nexatron API key (nxa_…) always authenticates as api_consumer whatever scopes it carries, so minting with one returns 403, and no combination of API-key scopes changes that.

Today that leaves two working paths: mint from the Get embed code dialog in the Nexatron dashboard UI, or have your backend hold a tenant_admin session token.

A service-credential mint path is not shipped.

What an embed token can and cannot do​

It is handed to a browser on a page you control, so it is deliberately narrow:

Acts asThe viewer_user_id it names, never the admin who minted it. RLS, ABAC and column masking all resolve for the viewer.
ReachesPOST /api/v1/query, GET /api/v1/conversations, GET /api/v1/conversations/:id/messages, POST /api/v1/feedback. Nothing else: every other endpoint returns 403, including /api/v1/query/stream.
Carries at mostThe viewer's own permissions intersected with queries.execute, reports.read, semantic.read. Minting as a tenant_admin viewer does not put admin powers in the browser.
RefusesA token with no viewer_user_id (401), a token minted scopes: ["dashboard"] only (403), a viewer who cannot run queries (403), and a viewer whose account has been disabled (401).
Lives forOne hour, maximum, regardless of expires_in_hours.
Does not narrowWhich data source the question runs against. There is no per-connection token scope: Ask routes across every connection the workspace has, and may span several to answer one question. Confinement is the viewer's (RLS, ABAC and column masking), which cuts by row and column rather than by whole warehouse. If a viewer must not reach a source, restrict that viewer in Nexatron; do not expect the token to do it.

Subject-attribute ABAC policies are the one place an embed is stricter than the product: an embed token asserts no subject attributes, so a policy scoped by one fails closed on its tables rather than being skipped.

3. Wrap your tree in <NexatronProvider> (1 minute)​

// app/providers.tsx
"use client";
import { NexatronProvider } from "@nexatron/chat/react";
import { useEffect, useState } from "react";

export function Providers({ children }: { children: React.ReactNode }) {
const [token, setToken] = useState<string | null>(null);

useEffect(() => {
fetch("/api/nexatron/session", { method: "POST" })
.then((r) => r.json())
.then((d) => setToken(d.token));
}, []);

if (!token) return <div>Loading Nexatron…</div>;

return (
<NexatronProvider
config={{
baseUrl: "https://api.nexatron.io",
token,
}}
>
{children}
</NexatronProvider>
);
}

4. Drop <NexatronChat /> into any route (1 minute)​

// app/analytics/page.tsx
import { NexatronChat } from "@nexatron/chat/react";

export default function AnalyticsPage() {
return (
<main style={{ height: "70vh", maxWidth: 720, margin: "0 auto" }}>
<NexatronChat placeholder="Ask about your data…" />
</main>
);
}

That is the full integration. The component renders messages, handles loading + error states, manages the conversation ID, and auto-scrolls. Submit "show me revenue by region for Q4" and you should see the complete answer appear when it is ready, with the generated SQL and its trust label. The SDK does not stream, and an embed token cannot reach /api/v1/query/stream.

5. Theme it to match your brand (5 minutes)​

<NexatronChat /> ships with a neutral default look that should fit most apps. Two ways to customize:

Option A: Inline theme prop​

Best for one-off styling adjacent to the embed:

<NexatronChat
theme={{
accent: "#7c3aed",
bg: "#0f172a",
text: "#e2e8f0",
userBg: "#1e293b",
assistantBg: "#0b1220",
border: "1px solid #334155",
radius: "12px",
}}
/>

Option B: Global CSS​

Best when the embed is consistent across the app. The component declares CSS custom properties on the .nx-chat class; override them anywhere in your stylesheet:

/* your app's global stylesheet */
.nx-chat {
--nx-accent: #7c3aed;
--nx-bg: #0f172a;
--nx-text: #e2e8f0;
--nx-user-bg: #1e293b;
--nx-assistant-bg: #0b1220;
--nx-border: 1px solid #334155;
--nx-radius: 12px;
}

The full theme surface​

Token (prop)CSS variableDefaultWhat it controls
border--nx-border1px solid #e5e7ebOuter container border + input/form separator
radius--nx-radius8pxOuter container border-radius
bg--nx-bg#ffffffContainer background
text--nx-text#0f172aPrimary text color
muted--nx-muted#64748bHint / timestamp text
accent--nx-accent#5B4FE5Send button + focused input outline
userBg--nx-user-bg#EFEDFFUser message bubble background
assistantBg--nx-assistant-bg#f8fafcAssistant message bubble background
errorText--nx-error-text#b91c1cError banner text color

Custom UI​

If <NexatronChat /> does not fit your design, drop down to the hook. The hook gives you state, dispatch, and lifecycle; you supply the layout:

import { useNexatronChat } from "@nexatron/chat/react";

function CustomChat() {
const { messages, sendMessage, isLoading, error } = useNexatronChat();
// …your own JSX
}

See the SDK API reference for the full hook signature.

Web Components (non-React)​

Drop <nexatron-chat> into a plain HTML page:

<script type="module">
import { defineNexatronChat } from "@nexatron/chat/vanilla";
defineNexatronChat();
</script>

<nexatron-chat
api-url="https://api.nexatron.io"
api-key="<short-lived-embed-token>"
></nexatron-chat>

<style>
nexatron-chat { --nx-accent: #7c3aed; }
</style>

The Web Component uses the same --nx-* CSS variable surface, so a theme that works in one place works in the other.

What happens on the wire​

User in your React app
│
▼ sendMessage("…")
NexatronClient (browser, with embed token)
│
▼ POST /api/v1/query
Nexatron API
│
▼ answers as the signed-in person, under row-level security
│
▼ row-level-security filtered results
Browser
│
▼ ChatMessage appended to messages array
<NexatronChat /> re-renders

Every request carries the embed token, the SDK never holds a long-lived credential, and both tenant and per-viewer row-level security run on the Nexatron side, so a token minted for tenant A cannot read tenant B's data, and a token minted for one viewer cannot read another viewer's rows, even if the browser forges the request.