Access Rules
Decide what an app's visitors and an agent's tools may read and change in the database, files, and Retrieval of an environment, and in apps also in runs, sessions, and approvals. Rules run on every request and tool call, and anything without a rule is denied.
On this page
Apps and agents use the same access rules. access in apps/<key>.yaml or agents/<name>.yaml holds one rule per resource and operation: an expression about who asks and the record.
- An app's rules check every request of its data API. An app without
accesscan't read or change any data. The app's Access tab in the dashboard shows the rules that are deployed. - An agent's rules check every call of the database, file, and Retrieval tools, also when your own tools call them through
connic.tools. Withoutaccess, these tools can use all data of the agent's environment. See Agents.
Rules
lookups:
member:
collection: members
where: doc.email == user.email
access:
database:
tickets:
read: doc.author_id == user.id or member.role == "agent"
insert: user.signed_in == true
update: doc.author_id == user.id and doc.status == "open"
delete: member.role == "admin"
set:
author_id: user.id
status: '"open"'
files:
attachments:
read: member.role == "agent"
runs:
read: run.context.end_user.id == user.id
retrieval:
help:
search: trueaccess:
database:
"*":
read: true
tickets:
read: user == null or doc.author_id == user.id
insert: true
update: user == null or doc.author_id == user.id
files:
uploads:
read: file.session_id == session.id
write: true
retrieval:
help:
search: true
notes:
search: true
store: true| Resource | Keys | Apps | Agents |
|---|---|---|---|
| database | collection name, or "*" | read, insert, update, delete, set, fields | read, insert, update, delete, set, fields |
| files | folder, or "*" | read, write, update, delete, set | read, write, update, delete, set |
| retrieval | namespace, or "*" | search | search, store, delete |
| runs | read, cancel | ||
| sessions | read, delete | ||
| approvals | read, decide |
A missing resource, collection, folder, namespace, or operation is denied. YAML true and false are rules too: read: true lets everyone read. Quote a rule that starts with a quotation mark, as in read: '"finance" in user.groups', so YAML reads it as text.
The entry "*" covers every collection, folder, or namespace without an entry of its own. A named entry wins over it, even an empty one: secrets: {} next to "*": {read: true} denies every operation on secrets. Quote the key, because YAML reads a bare * as an alias.
Names
Each rule sees user, the lookups, and the record of its operation; agent rules also see context and session. Using a name that isn't available fails connic lint.
| Name | Available in | Fields |
|---|---|---|
| user | every rule | apps: the visitor, with id, email, name, groups, claims, signed_in. Agents: the app visitor the run acts for, with the same fields, or null |
| context | agent rules | the run's context |
| session | agent rules | id of the agent's current session; null without a session |
| <lookup> | every rule except a lookup's where | the document's fields plus id, created_at, updated_at; null when nothing matched. With values: a list |
| doc | database read, update, delete, fields | the stored fields plus id, created_at, updated_at |
| new | database insert, update; files write, update | the document or file after the write, including the fields set writes; see How rules apply |
| file | files read, update, delete | path, folder, name, size, mime_type, metadata, session_id, created_at, updated_at |
| run | app runs read, cancel | id, agent, status, context, input, output, queued_at, ended_at, duration_ms |
| session | app sessions read, delete | id, agent, state, created_at, updated_at |
| approval | app approvals read, decide | id, agent, tool, params, status, context (the run's context), created_at |
In apps, user is the visitor; see Sign-in for what id, groups, and claims hold in each mode. run.input and run.output can be read as JSON, as in run.input.order_id. Runs started from an app carry the visitor in run.context.end_user, and approvals carry it in approval.context.end_user.
In agents, user is the visitor of the app whose request started the run, also in the agents that run starts with trigger_agent. connic takes it from the signed app request, so middleware and tools can't change it. Runs no app visitor started, such as from cron, a webhook, or the dashboard, have user null.
Agents
Rules in agents/<name>.yaml decide what the agent's built-in tools may do, whether the model calls them or your own tools call them through connic.tools. A denied call returns an error instead of a result, and lists and counts only see allowed records.
- Database tools use
databaserules.db_upsertneedsinsertandupdate, and is denied when its filter matches only documents the rules hide. - File tools use
filesrules, and so do browser downloads into files, browser uploads from files, files thatretrieval_storeimports, and share links inretrieval_queryresults. - Retrieval tools use
retrievalrules:searchforretrieval_query,storeforretrieval_store, anddeleteforretrieval_delete. A search without a namespace needs the entry"*". - Once an agent has
access,connic lintfails when it lists a built-in tool whose resource has no entries, because every call would be denied. Each resource takes up to 100 entries. - Rules on
userdeny runs without a visitor. Writeuser == null or ...to let those runs through, and keep in mind that asetvalue that readsuserdenies the write when there is no visitor. lookupssit next toaccessin the agent file. Theirwherecan also usecontextandsession, and a lookup can't be calledcontext.
Deploys check agent rules like app rules and fail with the agent file and the rule's path. See Replace database, retrieval, and files for agents that still use the older settings.
Grammar
Rules use connic's expression language, the same one as approval rules and conditional tools: comparisons, and, or, not, in, not in, paths with dots or subscripts, and literals, without function calls or arithmetic, up to 2,000 characters. Because connic turns the record part of a rule into a query filter, these limits apply to the record names doc, file, run, session, and approval:
- Compare a field with a value, as in
doc.done == true. A baredoc.doneis rejected. - A rule can't compare two fields of the record, or a field of the record with
new. - In database
updaterules, usenewthrough its fields, as innew.status, and compare each with a value: two fields ofnewcan't be compared. doc.id,file.session_id,run.id,run.status,approval.id, andapproval.statustake only==,!=,in, andnot in.- Field names can't start with
_or$or contain dots. Stored system fields areid,created_at, andupdated_at. The same goes for the names undersetandfields, and for each part of a lookup'svaluespath, which may have several parts, as inteam.id.connic lintand the deploy reject other names.
Every rule also follows these limits:
isandis notwork only with the literalsnull,true, andfalse.- A comparison can't take another condition as a value, so
(doc.done == true) == falseis rejected. Combine comparisons withand,or, andnot. - Database and file rules, and the
whereof lookups, become database filters with these limits: 10 levels of nesting, of which the filter connic wraps around access rules takes one, 100 branches perandoror, 256 terms, and 1,000 values perinlist.
connic lint checks syntax and names. Deploys of public apps warn in the deployment log about rules that let every visitor read everything, write without depending on the visitor, or upload without a size limit. The deploy checks that connic can enforce every rule and fails with the rule's path, for example with use is and is not only with null, true or false or it compiles to a database filter that is too large: <reason>:
App validation failed:
- apps/portal.yaml: access.database.tickets.read: can't compare two fields of docHow rules apply
For each request or tool call, connic evaluates every part of a rule that doesn't involve the stored record: user, the lookups, new, and in agents context and session. What's left about the record becomes a filter that connic adds to the caller's own. Lists, counts, totals, updates, and deletes therefore only ever touch allowed records.
- A rule that is false whatever the record denies the request, with 403 in apps. For a single run, file, session, or approval, a record the rule doesn't allow answers 404, like one that doesn't exist.
insertand fileswritechecknew, the document or upload with the fieldssetwrites, before the write. Every document of an insert must pass, or nothing is inserted.- Database
updatechanges only the documents thedocpart allows.newis each document after the change: the fields the update sends, and the stored values for the rest. A field the update sets tonullis gone fromnew. Soupdate: new.status == "open"lets an update of the title through on open tickets. - Files
updatesees the file before the change asfileand after it asnew. Moving a file to another folder also needs that folder'swriterule. - An upsert needs
insertandupdate. It updates the first document the rules let change, or inserts one the insert rule allows, and is denied when its filter matches only documents the rules don't let change. - A rule can still make a filter that is too large for one request, for example
doc.team in user.groupsfor someone in more than 1,000 groups. The request then fails, in apps with 500An access rule makes a database filter that is too large: <reason>. Simplify the rule or the values it compares with.
Missing values and null
A comparison is unknown when one of its sides is missing, such as an absent field or a field of a lookup that found nothing, or when it is null. not leaves unknown unknown. and is false when any part is false, or is true when any part is true, and otherwise an unknown part makes the result unknown. A rule that ends unknown denies. So not (member.blocked == true) denies a visitor without a member document.
Only a comparison with the literal null tests for null. doc.x == null and doc.x is null are true when the field exists and is null, false when it holds another value, and unknown when it is missing; != and is not give the opposite for an existing field. A lookup that found nothing counts as present and null, so member == null is true, and so is user == null in an agent run without a visitor. doc.status != "archived" matches only documents whose status holds another value; add or doc.status == null to include those where it is null.
Comparisons
- Values compare as JSON. Each type equals only its own:
trueisn't1, and"1"isn't1, but1equals1.0. <,<=,>, and>=compare two numbers or two texts, texts by code point. Any other pair is unknown.x in listchecks the list's items with the same typed equality, and an empty list gives false.x in objectchecks the object's keys.x in textis true only when x equals the whole text, never for a part of it, so"vip" in doc.tagsfits a list of tags. In anything else,inis unknown.- Timestamps, such as
doc.created_at,file.updated_at,run.queued_at, orapproval.created_at, compare as points in time with ISO 8601 text, as indoc.created_at >= "2026-01-01T00:00:00Z". Text without an offset is UTC. - A value on its own as a condition, such as
user.signed_in, must betrueorfalse; anything else is unknown.
set
set lists fields that connic writes itself, over whatever the caller sent. A plain value writes on insert. The form with value and writes also writes on every update, or only then. Under files, set writes metadata: on insert means on upload or file_write, and on update means on every files update, renames included.
access:
database:
tickets:
set:
author_id: user.id # on insert
updated_by:
value: user.id
writes: [insert, update]
files:
uploads:
set:
uploaded_by: user.id # metadata of each uploadEach value is an expression over user and the lookups, and in agents also context and session. So author_id: user.id records who wrote a document, and status: '"open"' writes a fixed text. When a value can't be evaluated, for example because the lookup it reads found nothing, the write is denied. On insert, connic overwrites the set fields the caller sent, and an insert that sends a field connic writes only on update fails. An update that names any set field fails, in apps with 400 Field 'author_id' is set by the app and can't be changed.
fields
fields holds a rule per top-level field of a collection. The rule sees doc, so it can depend on the document as well as on who asks.
access:
database:
tickets:
read: doc.author_id == user.id or member.role == "agent"
update: doc.author_id == user.id or member.role == "agent"
fields:
internal_notes: member.role == "agent"- Reads return the field only from documents where its rule holds. Other documents come back without it.
- The rule is checked against the document as returned. When a find's
fieldslist leaves out a field the rule reads, the field it guards is left out too, so include the fields your field rules read. - A filter, a sort, or a distinct on a field whose rule doesn't hold for every document is denied, so a field can't be found out by searching for its value.
- An insert that sends the field needs its rule to hold for the new document. An update that sends the field changes only the documents where its rule holds.
- Fields that
setwrites don't need a rule here. Fields without a rule are read and written under the collection's rules alone.
Lookups
A lookup finds one document in a collection for rules that depend on stored data, such as a member's role. collection names the collection, and where is an expression over doc and user, and in agents also context and session. connic runs a lookup only when a rule uses it, at most once per request or tool call, and takes the oldest matching document. The lookup collection needs no access rule of its own, and the caller can't read it unless one allows it.
With values, a lookup is the list of one field's values in every matching document instead, for rules such as membership in several teams. Documents without the field add nothing. A lookup that matches more than 1,000 documents fails the request with The lookup <name> found more than 1000 documents. Narrow its where.
lookups:
teams:
collection: memberships
where: doc.user_id == user.id
values: team_id
access:
database:
projects:
read: doc.team_id in teamsLookup names are identifiers, such as member, and can't be user, doc, new, file, run, session, approval, true, false, or null, and in agents not context.
Folders
Keys under files are folders, with or without slashes. A rule covers its folder and everything below it, and the most specific folder wins: with rules for reports and reports/private, files in reports/private/2026 follow reports/private. The entry "*" covers the whole tree, the root folder included, wherever no other entry does.
A list shows one level: the files of the folder that its rule lets the caller read, and the subfolders that hold at least one file the caller can read under that file's own rule. Files under a folder whose rule denies reading stay hidden, unless a more specific folder below has a rule that allows them. Listing a folder that no entry covers returns no files but the subfolders that hold readable files. Only when no rule at or below the folder can allow reading is the list denied.
Retrieval namespaces
Keys under retrieval are Retrieval namespaces, such as help or docs.public. A namespace follows the rules of the most specific entry that holds it, and "*" covers the rest. The rules see who asks and the lookups, not the entries.
A search returns passages from the namespace searched and every namespace below it, so it covers them whatever their own rules: with help and help.internal listed, a search in help also returns passages from help.internal. Keep content for different audiences in separate namespaces, such as public.help and internal.help. store and delete follow the entry of the namespace they write to. Apps always search in a namespace, so entries without one can't be searched from an app.
Public visitors
On a public app, user.signed_in is false, user.email and user.name are null, user.groups is empty, and user.id is the browser's visitor ID.
Comparisons with a public visitor's email are unknown, so doc.owner == user.email matches no document, not even one whose owner is null, and a lookup with where: doc.email == user.email finds nothing.
connic derives the visitor ID from a secret in the browser's cookie, separately for each app, so knowing someone's ID doesn't let anyone act as them. The ID changes when the cookies are cleared, and anyone can get a new one. Rules on user.id fit data that belongs to one browser, such as a draft, not data that needs an account.
Protect public forms
Anyone can submit the forms of a public app, bots included. Writes through the data API are checked against your rules but run no middleware, so they can't check a challenge. For forms that bots target, protect the form yourself and let an agent do the write:
Add a challenge service, such as a CAPTCHA, to the form. It adds a token to the submission.
Send the form to a sync webhook connector with
apps_only: trueat/_connic/c/<key>, instead of inserting it through the data API.In the agent's middleware, verify the token with the challenge service and stop the run when the check fails. The agent then writes with database tools.
Leave
insertout of the collection's rules in the app, so the browser can't write past the check.
import os
import httpx
from connic import StopProcessing
async def before(content, context):
token = context["payload"].get("challenge_token")
async with httpx.AsyncClient(timeout=10) as client:
response = await client.post(
os.environ["CHALLENGE_VERIFY_URL"],
data={"secret": os.environ["CHALLENGE_SECRET"], "response": token},
)
if not response.json().get("success"):
raise StopProcessing("The form check failed. Reload the page and try again.")
return contentThe verification request depends on the challenge service; the example sends the token and a secret to the service's verification URL, both stored as Variables. StopProcessing ends the run before the agent starts, and the form gets the message as the run's output. A rate_limit on the connector caps how many runs the form can start per minute. connic doesn't check challenges itself.
Replace database, retrieval, and files
Agents can still limit their tools with the older database, retrieval, and files settings, which are deprecated. connic enforces them as the access rules below. connic lint warns about them and prints the matching access block to paste in their place, and deploys warn in the log. An agent can't have both access and the older settings.
| Older setting | Access rules |
|---|---|
| database, files, or retrieval left out next to the others | an entry "*" with every operation |
| no collections, folders, or namespaces listed | the settings go into an entry "*" |
| collections: [orders] | orders: read, insert, update, delete |
| prevent_write: true | database without insert and update, files without write and update, retrieval without store |
| prevent_delete: true | without delete |
| session_only: true | read, update, delete: file.session_id == session.id; write: session.id != null |
A setting on an entry wins over the top-level one, and nested namespaces follow the most specific entry.
Examples
Own records
Signed-in visitors see and change only the notes they created. set stamps the owner on insert, and updates can't change it.
access:
database:
notes:
read: doc.owner_id == user.id
insert: user.signed_in == true
update: doc.owner_id == user.id
delete: doc.owner_id == user.id
set:
owner_id: user.idAdmins through a lookup
On an app that requires sign-in, a members collection, maintained by agents or imported, holds each person's customer and role. Customers see their own orders, and admins see and change all of them.
lookups:
member:
collection: members
where: doc.email == user.email
access:
database:
orders:
read: doc.customer_id == member.customer_id or member.role == "admin"
update: member.role == "admin"An agent working for a visitor
The orders app above calls a support agent. When a visitor starts the run, the agent reads and changes only that customer's orders. Runs from cron or a webhook, without a visitor, reach every order.
tools:
- db_find
- db_update
lookups:
member:
collection: members
where: doc.email == user.email
access:
database:
orders:
read: user == null or doc.customer_id == member.customer_id
update: user == null or doc.customer_id == member.customer_idFiles per session
Each customer has their own session, and the agent sees only the uploads written in the current one. Templates are shared and read-only, and nothing can be deleted.
session:
key: context.customer_id
tools:
- file_list
- file_read
- file_write
access:
files:
uploads:
read: file.session_id == session.id
write: true
update: file.session_id == session.id
templates:
read: truePublished content on a public app
Anyone can read published articles. Drafts stay hidden, and nobody can write.
auth:
mode: public
access:
database:
articles:
read: doc.status == "published"Files by group and metadata
The billing group reads invoices. Every visitor uploads PDFs of up to 10 MB into uploads, tagged with their own ID from /_connic/me, and sees only their own uploads. With set: {uploaded_by: user.id} under uploads, connic writes the tag itself.
access:
files:
invoices:
read: '"billing" in user.groups'
uploads:
read: file.metadata.uploaded_by == user.id
write: >-
new.metadata.uploaded_by == user.id
and new.mime_type == "application/pdf"
and new.size <= 10000000Runs the visitor started
Visitors follow the runs they started through the app's connectors and cancel them while they wait or run.
access:
runs:
read: run.context.end_user.id == user.id
cancel: run.context.end_user.id == user.id and run.status in ["queued", "started"]Approvals for a group
The finance group sees every approval of the environment and decides refunds under 5,000.
access:
approvals:
read: '"finance" in user.groups'
decide: '"finance" in user.groups and approval.tool == "issue_refund" and approval.params.amount < 5000'Help center search
Anyone can search the articles in help and the namespaces below it through retrieval/search, and open their kept files through files/download. On an app that requires sign-in, a rule such as search: '"support" in user.groups' limits search to a group.
auth:
mode: public
access:
retrieval:
help:
search: true
files:
retrieval/help:
read: true