.tql files in your Ontology Library. When someone views the app, every live query renders that file on the server as the person looking, with their identity, their roles, and their Library access. The app’s own code only presents what comes back, so it cannot widen what a viewer sees.
How it works
A governed.tql file can read the viewer’s identity through runtime values. No parameters need to be declared:
These values come from the server, never from the browser, so a viewer cannot choose or alter them. The viewer also needs read and execute access to the
.tql file in the Library. A viewer without that access is refused, even if the app’s author has it.
Embedded viewers are identified by their attributes, not by a TextQL account. A viewer of a per-viewer embed link has no roles, and has a
tql_user_email only if your signed viewer assertion includes one in client_attributes. A session that runs on an API key carries the key’s roles and the email of the key’s owner, which is usually a service account rather than the person viewing. Write rules for embedded viewers on _tql.client_attributes_json fields, as in Embedding Data Apps.Step 1: Model your entitlements
Keep a table that maps each person to what they are entitled to see: a health system, a region, a tenant, or a list of accounts. Key it on the identity you will read at query time, usually email. The table does not have to live in the same warehouse as the data it scopes, because TextQL can join across connectors.Step 2: Write the governed query
Read the viewer’s identity, check their role, and require a matching entitlement on every path to the data. This query returns a provider’s own patients and refuses anyone without the provider role:EXISTS check keeps each patient counted once even if a person has more than one entitlement row for the same scope.
Write every rule so it fails closed:
- Reading a missing attribute fails the render, so a query that needs an identity never runs without one.
- Use
errorwhen a role check fails. Never fall back to an unfiltered query. - Require a matching entitlement on every path to the underlying data.
Step 3: Use it in your app
Declare the file as alibrary_tql data source, or reference it from a compute function with tql_path.
A source whose .tql, or anything it imports, reads _tql is live-only:
- It is never baked into the app’s shared snapshot, so
ana.data.gethas nothing for it. - Read it from a compute function, either with
ana.query("source_name")in Python or as atql_pathfunction, and call that function from the app withana.compute.run. - Render the rest of the app from the snapshot first, and hide the section that needs the viewer when nobody is signed in.
What each viewer sees
Where entitled rows never appear
- The app’s shared snapshot and the cached copies built from it.
- Public share links and screenshots.
- Queries that read the viewer’s identity inside scheduled jobs and refresh hooks that run as the app’s creator. Those queries are refused.
Dashboards
Dashboards follow the same rules. Live queries from dashboard code withana.query render as the viewer, and a source that reads _tql is not preloaded as a dashboard_<name> DataFrame. Results cached by ana.query and by @st.cache_data or @st.cache_resource functions are keyed to each viewer, so one viewer’s cached rows are never returned to another.
Revoking access
A change to someone’s roles or entitlement rows takes effect the next time their query runs on the server. Results that are already cached can still be served until they expire:- In an open dashboard,
ana.queryresults expire within 1 minute and cached functions within 5 minutes. This applies to role changes as well as entitlement changes. - In a Data App with its query cache turned on, a change to the entitlement table can take up to the cache lifetime set for the app, at most 15 minutes. Roles are applied when the query renders, before that cache is consulted, and the rendered SQL is part of the cache key, so a role change applies on the next query.
Testing your rules
Before rolling an app out, check it as each kind of viewer:- Sign in as a person from each scope (for example, one provider per health system) and confirm each sees only their own rows.
- Sign in as someone with a role but no entitlement row, and confirm they see nothing.
- Sign in as someone entitled only to aggregates, and confirm row-level detail is refused.
- Open a public share link, and confirm the protected section is hidden.
- Run any scheduled jobs, and confirm none of them write entitled rows into shared output.
Related
- Embedding Data Apps for per-user security in apps embedded in your own product.
- Embedding Dashboards for per-viewer embed links.
- TQL Reference for every runtime value.