Govern & operate
Access control
Provision people, put them in groups, control which models they may run, and share data read-only — enforced in the database, not just hidden in the interface.
Open Admin → IAM (superadmins only). Everything here is enforced by row-level security in the database, which is the reason it holds: an agent, an API call and the UI all hit the same rules, so there is no path that quietly bypasses them.
The seven tabs
| Tab | What you do there |
|---|---|
| Users | Invite, create, ban, delete; grant/revoke superadmin; manage group membership |
| Groups | Create, rename, delete groups and manage members |
| Access | Model rules and resource grants |
| Attributes | Key/value pairs pinned to a user, referenced from row filters as {{user.<key>}} |
| Budgets | Per-group spend caps — see Budgets |
| SSO | SAML identity provider configuration |
| Settings | Public-signup toggle, the default model-access policy (allow vs deny), trace retention, and the current superadmin list |
Roles
There are two: superadmin and everyone else. The account named by ADMIN_EMAIL is the permanent bootstrap superadmin — it keeps the role even if its row is deleted, so you cannot lock yourself out. Superadmins can promote and demote others.
Note
Users
Two ways to add someone:
- Email invitation — they set their own password. Preferred; no credential ever passes through you.
- Manual with a temporary password — for when email delivery isn't configured or you're provisioning in bulk. Have them change it.
Users can be banned (blocks sign-in, preserves their content) or deleted. Ban first when someone leaves — deletion is not reversible.
Groups
A named set of users. Model rules and resource grants attach to groups, so onboarding becomes "add to Engineering" rather than a dozen individual grants — and offboarding becomes one removal.
Model rules
Control which models a person may run. What "no rules" means is an instance choice — Settings → Default model access:
| Situation | Allow by default (the default) | Deny by default |
|---|---|---|
| User has no rules, and no group with rules | Unrestricted — every model available | No models at all, until a rule allow-lists them |
| Rules apply (their own or a group's) | Allowed = the union of those rules | Allowed = the union of those rules (identical) |
| Superadmin | Rules apply to them like anyone else | Bypasses entirely — the lock's administrator can't be locked out |
Rule is openai + * | Every model from that provider | Every model from that provider |
Rule is openrouter + openai/* | Prefix match — those models only | Prefix match — those models only |
Flipping to deny is safe to stage
Why it works this way
Enforcement is server-side at the point every chat request is dispatched, so it covers the playground, saved agents, swarm nodes, API runs — and public embeds, which execute their owner's stored model for anonymous visitors and are checked against the owner's effective rules on every request. Pickers also filter to allowed models, but that's convenience — the check that matters happens on the request. Disallowed models return a clear error rather than failing obscurely.
Resource sharing
Grant a user or group read-only access to a resource owned by someone else, under Admin → IAM → Access. Eleven resource types are grantable, enforced by a database constraint:
| Type | What the grantee gets |
|---|---|
| 📚 Knowledge base | Their agents can search it |
| 🗄 SQL data table | Queryable, subject to row filters and column masks |
| 🔑 Secret | Usable by reference; the value is never shown |
| 📊 BI dashboard | Viewable, subject to row filters and column masks |
| 🧮 Semantic model | Its metrics and dimensions become askable |
| 🗂 Data catalog source | Its crawled tables and profiles become browsable |
| 🤖 LLM key / ☁️ LLM credential | Calls bill to the owner's key |
| 🏢 Database / warehouse connection | Queryable — see below |
| 🔌 App source | Syncable — see below |
| 🧠 AI analyst | Usable — but as the grantee, not the owner; see below |
Recipients see a Shared badge; edit and delete controls are hidden and writes are blocked by the database regardless.
Because most grants are enforced in row-level security, agent tools inherit them automatically — a shared table becomes queryable by that user's agents with no extra wiring.
Narrowing a grant: row filters and column masks
Sharing a dataset or dashboard does not have to mean sharing all of it. Two optional restrictions can be attached to a grant on a SQL data table or a BI dashboard — the two types that serve rows. Neither applies to the other eight, and the platform refuses to save them there.
| Restriction | Shape | Effect on the grantee |
|---|---|---|
| Row filter | a column and a list of values | They see only rows where that column matches one of those values |
| Column mask | a list of column names | Those columns are removed server-side — from the column list and from every row |
A regional lead granted the sales table with a row filter of region ∈ (EMEA) and a column mask of salary can query the table freely, see only EMEA rows, and never receive the salary column in any answer.
Both are applied on the server, before the data leaves it — on stored dashboard snapshots, on live warehouse queries, and on the rows an agent's SQL tool reads. There is no path that applies one and not the others.
One grant, per-viewer rows: user attributes
A row filter's values do not have to be literals. Writing {{user.<key>}} in place of a value resolves it, at query time, to the calling user's own values for that attribute — so a single grant on a group scopes every member to their own slice, instead of one grant per person.
Attributes are key/value pairs pinned to a user on the Attributes tab. They are admin-written only: nobody can set their own, which is what makes them safe to filter on. An attribute holds a list, so a manager covering two regions simply has both values.
| Grant on the group | Alice (region = EMEA) | Raj (region = APAC, LATAM) |
|---|---|---|
region ∈ [{{user.region}}] | EMEA rows only | APAC and LATAM rows |
A missing attribute refuses the query
{{user.region}} and the user has no region, the query is refused, naming the attribute — it does not quietly return zero rows. Both of the obvious alternatives are worse: empty results read as "there is no data for you", and an unresolved token passed through as a literal matches nothing while looking like a real value. A refusal that names the missing key is the only outcome that tells the admin what to fix.Resolution happens per grant, before grants are merged, so two grants resolving to different values union exactly as two literal grants would. Setting the attribute is what makes an attribute-scoped share usable — see the semantic layer for how the same tokens apply to governed models.
Grants add up — they never subtract
A masked column is also unfilterable
A restriction that cannot be checked returns nothing
Aggregated widgets go empty for a filtered grantee
SELECT product, sum(revenue) FROM sales GROUP BY product has no region in its output, so a grantee filtered to region ∈ (EMEA) sees an empty widget rather than a global total that ignores their filter. The fix is to project the filter column — group by region, product — so the rows can be vetted. An empty widget is the restriction working, not a broken query.Shared connections run as their owner
Connections are the exception to the row-level rule, because those rows carry an encrypted credential. There is deliberately no row-level policy granting a recipient access to them — that would let a grantee fetch the ciphertext straight from the API with their own token. Instead the grant is resolved server-side and the row is loaded with the service role, so a grantee gains the use of a connection without ever receiving it. {{secret:NAME}} references resolve as the owner too, never against the grantee's own vault.
A shared app source syncs as its owner, into the owner's datasets — so a grantee re-running a stale sync refreshes the real datasets rather than building a parallel copy under their own account. Sharing the source lets someone keep it healthy; to let them read the resulting data, share those datasets too. Grants are resolved fresh on every call, including scheduled runs, so revoking one takes effect on the next use. Full details in Data sources.
A shared AI analyst runs the other way round, and deliberately. It carries no credential of its own, so the grant conveys the right to use it and nothing else: the grantee's questions are compiled and run as them, under their own dataset grants, warehouse access, row filters and column masks. The same analyst can therefore answer differently for different readers — the share dialog says so before the grant is made, and warns when the recipients cannot reach the data it is scoped to. Saved analyses are not shared: a thread holds result samples fetched under its author's access, so recipients start their own. The grant is refused outright if the recipients' model rules do not allow the analyst's pinned model. Full details in BI Workspace.
Signup policy and SSO
- Invite-only
- Turn off public signup and the database itself rejects new accounts that weren't invited or admin-created. Enforced at the trigger, so it also covers OAuth signups.
- SSO
- Connect a SAML identity provider so people sign in with your corporate directory. Can be made the enforced path.
Worked example — onboarding an analytics team
- 1
Groups → New group
Name itanalytics. Groups are how you avoid granting things person by person. - 2
Users → Add user → Invite by email
They set their own password, so no credential passes through you. Repeat for the team, then add each to analytics from their row. - 3
Access → Model rules → principal: group analytics
Addopenai+gpt-4o-miniandopenai+gpt-4o. The team is now restricted to those two; everyone outside the group is still unrestricted unless a rule applies to them. - 4
Access → Resource grants
Grantanalyticsread access to therevenuedata table and theFinance policiesknowledge base. Their agents can now query both with no further wiring. - 5
Budgets → new cap, scope group analytics
Set a monthly USD ceiling before handing out access, not after the first surprise. - 6
Settings → turn OFF public signup
Do this before you share the URL. Invitations and admin-created users keep working.
A sensible rollout
- Sign in as
ADMIN_EMAILand confirm you have the IAM page. - Create groups that mirror how people actually work, not the org chart.
- Turn off public signup before sharing the URL.
- Set model rules on groups — start permissive, tighten with evidence from Analytics.
- Share the data collections teams need read access to, rather than duplicating them.
- Pair rules with budget caps: rules decide what, budgets decide how much.