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
writeorwrite_privatepermission. 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’sclientId. 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’sclientId and fail closed when identity is missing:
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)
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:Step 4: Embed
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.