Analytics security and limits
A saved query reads real data through a real database connection, so what happens automatically matters as much as what the query itself does. Every rule below is one of three kinds: enforced automatically, rejected outright, or left for you to guarantee yourself.
Scope every query
- A query belongs to exactly one app.
- An internal query is confined to the calling site's schema:
- the connection's search path — the list of schemas the database looks in for a table name — is pinned to that schema alone while the query runs;
- a statement naming another schema is rejected when it is saved; and
- a statement naming a table that is not a data table of the same app is rejected when it is saved.
- Caller parameter values are sent to the database separately from the query text, so they are treated as data and cannot change what the query does. See Placeholders and bound values.
Who can run and manage queries
Who can run a query depends on who the caller is. A site user must pass two checks, in order:
| Layer | Checks | Default | Denied response |
|---|---|---|---|
| 1. App role | The site user holds a role on the app that owns the query, beyond the default role every signed-in user has | Always checked, before the query is read | 403 — No app roles assigned |
| 2. Per-query policy | A rule attached to that specific query, created when the query is created and removed when it is deleted | Open to any caller who already cleared layer 1, until you add a rule | 403 — You do not have permission to execute query '<slug>', once a rule naming other roles exists |
There is no policy field in the create request — the query's policy is created automatically, with no rules, so every new query starts open to any site user who already holds a role on the app. To restrict who can run it:
- Decide which roles genuinely need to run it.
- After the query exists, open Policies in the app, edit that query's policy, and name those roles. Analytics doesn't do this for you.
- Test with one user who should be allowed and one who shouldn't, in the same site and app.
Members of the organization that owns the site — owners, admins, and other members — can run any saved query without an app role or a query policy check. Other users need a role on the app and must pass the query's policy.
If the policy service is unavailable when a query is saved, the query is still saved but may have no policy. A query with no policy can't be run by users who rely on a policy; they are denied.
Creating, listing, viewing, editing, and deleting saved queries currently
checks only that the caller is signed in to the site — not an app role or the
query's policy. Running a query does check roles and policies. Keep secrets out
of query text (use secret. references), and review your app's saved queries
regularly.
Rely on read-only enforcement
Analytics queries are read-only, enforced in two ways: a check when the query is saved, and a read-only database setting when it runs.
- When a query is saved, its text must be exactly one read statement for
every SQL connector, internal included. The check reads the whole statement,
so a write placed inside a subquery or a
WITHclause, or aSELECT ... INTOthat creates a table, is refused. Statements such asSET,COPY, andVALUESare refused because they are not a query. - When a query runs, each SQL connector runs it with a read-only database setting.
- Elasticsearch runs through the search template endpoint only, and a
query body containing a
scriptorruntime_mappingskey at any depth is refused when it is saved.
For every external connection, also use a database account that can only read.
Write a query that will save
| Rejected | Applies to |
|---|---|
| Empty query text | All connectors |
| More than one statement | Internal, PostgreSQL, MySQL, Redshift, and ClickHouse |
| A statement that is not a read, or contains a write anywhere inside it | Internal, PostgreSQL, MySQL, Redshift, and ClickHouse |
Functions that pause the database, read server files, reach other systems, or change a database setting (set_config) | Internal, PostgreSQL, MySQL, Redshift, and ClickHouse; the blocked list differs for ClickHouse |
A stored secret. placeholder outside a WHERE, JOIN ... ON, or HAVING condition | Internal, PostgreSQL, MySQL, Redshift, and ClickHouse |
A placeholder used as a table or column name, inside part of a quoted text, followed directly by ::, or containing a filter, expression, or control block | All connectors that apply to it; see Placeholders and bound values |
| A query body that is not valid JSON, or a placeholder used as a JSON key | Elasticsearch |
A script or runtime_mappings key at any depth | Elasticsearch |
| Referring to a schema other than the calling site's | Internal queries |
| Naming a table that is not a data table of the same app, or a statement whose tables cannot be worked out | Internal queries |
| Referring to a secret that does not exist for the site and app | All connectors |
| An external connection with no connection secret | All external connectors |
These checks run on every create and every edit, including an edit that only renames the query or changes its tags.
Handle secrets deliberately
Secret values referenced by a query are read from storage when the query runs. They are not accepted from callers, and they are not part of the stored query definition, which holds only the secret's key.
In SQL queries a stored secret can decide which rows are read but cannot be
placed where its value would be returned or used to sort: its placeholder is
allowed only in a WHERE, JOIN ... ON, or HAVING condition, and any other
position is refused when the query is saved. The built-in user profile is
exempt, because it describes the caller rather than stored data.
Treat permission to run a query as permission to see every resolved value its text can return.
Connection secrets and secret. values are cached for up to one hour. Saving
or deleting a secret normally clears the cache right away, but occasionally a
rotated credential can keep being used until its cached copy expires — up to
one hour.
When execution fails, Analytics applies best-effort redaction to resolved query secret values and password-like connection URLs before storing and returning the message. This doesn't catch everything: the connection's own credentials aren't checked, and database error messages can include host names or query details.
Account for execution records
Raw caller parameter values are stored in the current execution record, together with the external connection secret key, referenced secret key names, and final error text.
An error message returned by the database can quote a value it failed to accept.
Do not put credentials, tokens, personal data, or other sensitive values in caller parameters. Automatic redaction doesn't catch everything, so review error messages before you share them.
Bound every query
Add a fixed row limit to the query text rather than relying on connector defaults.
| Limit | Value | Guidance |
|---|---|---|
| Result rows | 10,000 by default. Fixed for internal queries; an external connection secret may override it with max_result_rows. A run over the maximum fails instead of returning a partial result | Use an explicit fixed LIMIT or Elasticsearch size and test the deployed connector |
| Statements | One statement per query, checked on save for every SQL connector | Send one reviewed read statement |
| Description length | Maximum of exactly 1,000 characters, checked on save | 1,000 is accepted; 1,001 is rejected |
Protect a query before you delete it
Deleting a query permanently removes its definition — the query text, its connection, and its name. There is no recovery path, no archive, and no disable state.
Before deleting:
- Remember that deletion checks only that the caller is signed in, not their app role.
- Keep a copy of the query text if you might want it back. Recreating a query with the same name produces the same slug as long as no other query in the site holds it, but you have to supply the query text again.
- Confirm nothing else calls the query by slug, because the slug stops resolving immediately.
See Delete a query for the complete Console confirmation procedure.
Share safe diagnostics
For a support request, capture:
- the execution key returned with the failed run;
- the query slug and the app that owns it;
- the connection mode, and the connection secret key for an external query;
- the parameter names supplied, without their values if any are sensitive; and
- the error category and a summary of the message with any sensitive details removed.
Do not include credentials, personal data, or infrastructure details in a report.
Understand suspension
If your organization's Analytics access is suspended — for example, for an
unpaid bill — listing, viewing, running, creating, editing, and deleting
queries all return 402 until it is restored.
Direct HTTP
Manage saved queries in TaruviBase Console and run them with the SDKs. The reference shows the HTTP request the SDKs send, for comparison.