Skip to main content
A Data App isn’t limited to sharing inside TextQL — you can embed it directly in your own product, so external users interact with it without ever logging into TextQL. By default, an embedded app authenticates with one shared API key, so every external viewer sees the same data. For apps where each embedded user should see only their own rows, TextQL supports per-user Row-Level Security (RLS) on the embed: a single app, scoped per session with a short-lived API key tied to that viewer’s identity. One app serves every external user — there are no per-user copies to create, no user data baked into the bundle, and no per-user URL to keep secret.

Prerequisites

Set these up before wiring in per-user RLS:
  • An API key that can create other API keys. Minting a session key per user (Step 2 below) is itself a call to the TextQL API, so you need one master key with API Access Key write or write_private permission. See Creating an API Key for how to request one. Keep this key server-side only — it never reaches the browser.
  • A dedicated embed role. A key can only mint session keys that assume roles it already holds, so create a role scoped to what an embedded viewer should be able to do — typically read-only, with raw SQL disabled.
  • A data entitlement mapping. Row-level scoping only works if there’s already a governed way to resolve a user’s identity to the rows they’re allowed to see (an access.principal / access.secured_orders-style entrypoint keyed on email, account, or region, for example). This mapping doesn’t have to live in your ontology, and it doesn’t have to sit in the same warehouse as the data it’s scoping — TextQL can join across connectors, so an entitlement table in one warehouse can gate access to data in another. Build the mapping first — the app’s data sources reference it, they don’t create it.
  • A backend of your own. Minting keys and seeding the session chat (Steps 2–3 below) both happen server-side, so you need a backend that can call the TextQL API and hand the resulting embed URL to your front end.

How It Works

Your backend mints a short-lived API key per user session and sets that user’s identity in the key’s clientId. Because the key is created server-side, the browser can never choose or forge the identity. The app’s data sources are governed queries that resolve the viewer’s identity from the session key and filter at query time — nothing user-specific is baked into the app, and an access change (an expired key, a revoked entitlement) takes effect on the very next query.

Step 1: Scope the App’s Data Sources to the Session Identity

Write the app’s data sources as governed queries that resolve the viewer from the key’s clientId and fail closed when identity is missing:
There’s deliberately no fallback identity here: if the session key has no clientId (or client_attributes_json.username is otherwise blank), access.principal errors and the query returns zero rows rather than reaching for some other identity — a malformed or missing embed session must never see another viewer’s (or the app owner’s) data. Put the entitlements join on every path so no query can reach the underlying table unfiltered.

Step 2: Mint a Session Key per User (Backend Only)

A key can only assume roles the minting key already holds. Keep raw SQL disabled for embed sessions so only the governed entrypoints above can run.

Step 3: Seed a Session Chat That Loads the App

The embed surface is a chat carrying the app as an artifact. Create a fresh one per session with a single instruction that loads the app and does nothing else:
This takes roughly 30 seconds and produces a clean thread whose only content is the app itself. The refresh action never bakes governed data into the app — it only re-runs the app’s declared sources under the session’s identity.

Step 4: Embed

The app opens automatically, scoped to that session’s user. The same page also gives that user a governed chat next to the app, subject to the same row-level security — treat that as a feature, or hide it by sizing the iframe to the app panel alone.

What Each User Sees

Security Model

  • Identity is set once, at key-mint time, on your server, and travels in a signed token the browser cannot read or alter.
  • Entitlements are evaluated at view time, so revoking a row changes what a user sees on their next load, immediately.
  • Keys expire on the TTL you choose and can be revoked individually.
  • The app bundle itself contains no row data, so a leaked bundle URL exposes layout, not rows.

Current Limitations

  • The embed opens inside a chat surface rather than a bare app-only page. A dedicated app-only embed route is on the roadmap; the flow above is the supported path today.
  • Seeding the session chat adds one assistant call (about 30 seconds) when a session starts. Mint and seed the key at login rather than at page render if that latency matters for your product.

Getting Support

If you have questions about embedding Data Apps or run into issues, reach out to support@textql.com or visit the customer support page.