Skip to main content
For a runnable version of everything below, see the embed-app example.
Embedding is available in the TypeScript SDK (@textql/sdk). The React component described here needs v1.4.0+; the server handler works from v1.3.8. On-prem, TEXTQL_SERVER_URL is read from v1.4.1+.
Two environment variables on your server:

Quickstart

Server — one catch-all route. This is the only place your API key lives.
Browser — in React, one import and one tag:
That is the whole integration. Both halves default to /api/textql, so nothing needs configuring on the happy path.

Your routes are the access control

The API key is org-wide, so it stays on your server and the browser talks only to your routes — it is never told which app it renders and cannot ask for a different one. But nothing in the handler knows who the caller is. Without an authorize hook, the app is visible to anyone who can reach the route.
Return false or throw to reject.

The React component

@textql/sdk/embed/react is a thin wrapper over the same custom element. Prefer it to the raw tag: JSX has no type for a custom element, and React cannot bind the events below because they are CustomEvents.
string
default:"/api/textql"
Where createEmbedHandler is mounted. Must match the handler’s basePath.
CSSProperties
The element has no intrinsic size, like an iframe. Give it a real height.
string
Applied to the host element.
(meta: TextqlAppMeta) => void
Metadata arrived: name, screenshotUrl, and the app’s compute functions. Depends only on your server, so it fires even if the app’s bridge never connects — use it to title your own chrome.
(meta: TextqlAppMeta | null) => void
The app’s runtime completed its handshake.
(error) => void
The app reported a runtime error. Already logged to the console by the element.

Server-side rendering

<TextqlApp /> carries the "use client" directive and loads the custom element in an effect, so it server-renders to an inert tag and upgrades on the client. That also code-splits the element out of your initial bundle.
Do not import @textql/sdk/embed/element directly in a Next.js app. It defines a class extending HTMLElement at module scope, so importing it on a server throws HTMLElement is not defined — including inside a "use client" component, which Next.js still renders on the server.
react is an optional peer dependency (^18 || ^19). Nothing else in the SDK needs it.

Other frameworks

It is a custom element, so there is no framework binding to install.
Without a bundler, serve the element from your own origin. It is one self-contained file — it imports nothing, so serving it is a copy:
Copying it keeps the version pinned in your lockfile, where your existing review and scanning already look. A CDN saves the copy for a prototype:
Pin the version if you do. Don’t ship the CDN form to a restricted network: it puts a third-party host in your script-src, adds an outbound request on every page load, and does not resolve air-gapped.
Express, Fastify, and bare node:http predate the Web Request object, so wrap the handler:
Outside React, the same three events are CustomEvents on the element:
element.meta holds the same value for a listener that attached late.

Sizing

Data Apps lay out against the full viewport — the same region they get inside TextQL — so a narrow content column breaks the app’s own layout, not the element. Full-bleed width and a real height are the safe defaults.

Compute functions

Apps that compute call back through your server. The handler relays them to POST {basePath}/compute and refuses any function name the app does not declare, so the route cannot become a generic runner for anything your API key can reach. The declared names arrive as functions on onMeta. An app with none renders exactly the same but never calls back, leaving the route unused.
TextQL rate-limits compute server-side and returns resource_exhausted. A production host should retry that with backoff.

Options

The handler serves three routes under basePath and returns null for anything else, so it composes with your own routing: On-prem: set TEXTQL_SERVER_URL to the plain host. The SDK appends /rpc/public itself.

Why the document is served from your origin

TextQL pins a published app to the one origin it was published for, twice over: So GET {basePath}/document fetches the app’s HTML server-side and re-serves it from your origin: your response carries no frame-ancestors, and hostOrigin is rewritten to your origin. A <base href> keeps every subresource loading from the CDN, which already serves them with Access-Control-Allow-Origin: *. This is a workaround, with costs worth knowing:
  • Every document load proxies through your server as no-store, so the entry document is not CDN-cached. Subresources still are.
  • The rewrite is pinned to a string emitted by TextQL’s app shell. If that shape changes, the handler returns a 502 telling you to upgrade rather than serving a document whose bridge will never connect.
  • The response carries content-security-policy: sandbox allow-scripts. Untrusted third-party HTML is being served from your origin, and that header is what keeps it out of your cookies and localStorage. If you reimplement this route, keep it.
Set rehostDocument: false to point the iframe straight at the signed CDN URL instead. That is the better path, and it works once your origin is allowed platform-side.

On-premise and restricted networks

Nothing in the integration needs a TextQL-operated origin at runtime, so long as your instance also stores rendered apps somewhere you operate. Point the handler at your instance and serve the element yourself, and every host the browser reaches is one you run:
Every client reads TEXTQL_SERVER_URL from v1.4.1+ — the embed handler, new Textql(), and createStreamingClient. An explicit serverURL still wins. On earlier versions the variable is ignored and the SDK silently defaults to app.textql.com; pass the host at each construction site instead — createEmbedHandler({ client: new Textql({ serverURL }) }).
Check that last pair before assuming air-gapped. rehostDocument re-serves the entry document from your origin, but the scripts and styles inside it still load from wherever your instance stores rendered apps — your own object storage on-prem, TextQL’s CDN against cloud. With the element self-hosted and rehostDocument on, the host page needs script-src 'self' and frame-src 'self' — the iframe’s src is your own {basePath}/document — plus style-src 'unsafe-inline', because the element writes its shadow-DOM stylesheet as an inline <style>. Add the asset origin to img-src for the poster.

What the bridge leaves out

The full host inside TextQL also carries per-member state, activity logging, presence, deep-link routing, realtime sockets, and asking the agent. The embedded element declares all of them off, which is a supported configuration — apps that render data and call compute functions work unchanged, while apps built around member state render but lose those features.