Access Control
Fine-grained permission rules that control who can read, write, and execute your app's resources.
Default: DENY
If no access rules are configured for a resource, all access is denied (except for the app owner). You must explicitly grant access.
On this page
Rule Types
Nine rule types are available. Combine them with all / any / not / deny groups (see Composition & Operators) for precise control over who can access what.
| Type | Who it matches | Example use case |
|---|---|---|
| public | Anyone, no login required | Public product catalog |
| authenticated | Any logged-in user | Members-only content |
| creator | The user who created the row/file | Users edit own profile |
| has_role | Users with a specific role | Admin-only actions |
| field_match | Row field equals user's profile field | Department-scoped data |
| user_property | User's profile field has specific value | Premium subscribers only |
| Specific email address | Single-user access | |
| has_entitlement | Users with a live paid-access entitlement key | Gate a feature behind a purchase |
| function | Custom logic — a backend function returns true/false (runs even for anonymous, logged-out users) | Bespoke eligibility checks |
| relationship | Access via a related row, reached through a link field | "Team members can see their team's data" |
Composition & Operators
A flat array like [ruleA, ruleB] is shorthand for "any of these" (OR). For AND / NOT / explicit-deny logic, or to reuse a rule set across resources, use a condition group instead:
// AND — both must pass
{ all: [{ type: "authenticated" }, { type: "has_role", roleId: "<id>" }] }
// OR — same meaning as a flat array
{ any: [{ type: "creator" }, { type: "has_role", roleId: "<admin-id>" }] }
// NOT — negates a condition (never grants a row filter; use sparingly on reads)
{ not: { type: "user_property", field: "banned", value: true } }
// DENY — always wins, overriding every allow elsewhere in the tree
{ any: [
{ type: "authenticated" },
{ deny: { type: "user_property", field: "banned", value: true } },
] }
// POLICY — reference a reusable rule set by id (Settings → Access Control → Policies)
{ policy: "<policy-id>" }Groups nest arbitrarily. A missing or cyclic policy reference denies access (fail-closed) instead of erroring.
Operators
field_match and user_property rules accept an optional op (default eq):
eq · ne · gt · gte · lt · lte · in · nin · contains · isEmpty · isNotEmpty
// Users whose "age" profile field is 18 or older
read: [{ type: "user_property", field: "age", value: 18, op: "gte" }]Where userField / field values come from
field_match.userField and user_property.field read the user's profileData — the custom fields you declare via authCustomFields. The built-in email and name are not profileData keys — for per-user ownership use { type: "creator" }, use userField: "id" / "email" (special-cased to the user's identity), or declare a custom field and set it per user.
Declare custom fields with the full shape — required included:
updateProjectAuth({
authEnabled: true,
authCustomFields: [
{ name: "tenantId", label: "Tenant", type: "text", required: false }
]
})
// set per user: createAppUser({ email, customFields: { tenantId: "acme" } })
// rule: read: [{ type: "field_match", rowField: "Owner", userField: "tenantId" }]rowField takes the row field's name (or its id — both resolve on every lane).
Roles
Roles are first-class entities scoped per project, with stable IDs. Rules reference role IDs — the same name ("editor", "admin") in two different apps is two different roles, no cross-app collision.
Defining roles
Open Settings → Access Control. Add roles like editor, vip, manager. Or ask the in-builder AI: "create a role called editor".
Assigning roles to users
From the Users tab — click a user and toggle role chips. Or set them at creation time with roleIds.
Reserved role: app-super-admin
Auto-seeded per project. Any user assigned this role bypasses every access rule. Cannot be renamed or deleted. Use sparingly — it's a deliberate full-access escape hatch.
In SDK
const me = await user.me();
if (user.hasRole(me, 'editor')) { ... }
if (user.isAppSuperAdmin(me)) { ... }UI hints only — actual gating is server-side.
Legacy name-based rules
Older rules may use { type: "has_role", role: "editor" } (a name instead of a roleId). This form still works, but it matches the platform's built-in user/admin classification or a legacy profile roles list — it does not read the roleId assignments made from the Users tab. New rules should use roleId as shown above.
Page Access Rules
Pages have a single operation: read (view). When a user navigates to a protected page, the edge Worker checks rules before serving HTML — unauthorized users are redirected to /login or shown an Access Denied page. A bundled client-side AuthGuard then re-checks the same rules during navigation.
Example: VIP-only page
read: [{ type: "has_role", roleId: "<vip-role-id>" }]Only users assigned the vip role can view this page. Everyone else is redirected to login (if signed out) or shown Access Denied (if signed in without the role).
Table Access Rules
Set separate rules for each operation on a table:
| Operation | What it controls |
|---|---|
| read | Who can view rows (getRows, getRow, searchRows) |
| create | Who can add new rows |
| update | Who can modify existing rows |
| delete | Who can remove rows |
Example: A blog posts table
read: [{ type: "public" }]
create: [{ type: "has_role", role: "editor" }, { type: "has_role", role: "admin" }]
update: [{ type: "creator" }, { type: "has_role", role: "admin" }]
delete: [{ type: "has_role", role: "admin" }]Anyone can read. Editors and admins can create. Creators and admins can update. Only admins can delete.
Field-level access
Beyond the four operations above, individual fields can carry their own read/write conditions (accessRules.fields.<fieldId>). A field with no rule stays fully readable/writable — restriction is opt-in. Auth-system fields (hashedPassword, reset/verify tokens, OAuth ids) are hard-denied on every read and write regardless of rules, role, or owner status.
// Only managers may read the Salary column; everyone sees the rest
accessRules: {
read: [{ type: "authenticated" }],
fields: {
"<salary field id>": {
read: [{ type: "has_role", role: "manager" }]
}
}
}Backend functions bypass these rules
ctx.tables in a backend function applies creator-scoping only — it does not run this rule engine, and ctx.service.tables bypasses even creator scoping. A table locked to admins here is still fully readable from your own backend code. The protection point for functions is access rules on the function itself — see the backend functions guide.
File Access Rules
File access rules control who can upload, view, and delete files. The same rule types apply.
| Operation | What it controls |
|---|---|
| create | Who can upload files |
| read | Who can view and download files |
| delete | Who can remove files |
Where to attach file rules
For files uploaded from the published app — pass accessRules inline (preferred):
await files.upload(file, {
accessRules: { read: [{ type: "creator" }] },
});For files uploaded from the dashboard or via MCP — backfill with files_setAccessRules (or files_setFolderAccessRules for bulk):
files_setAccessRules({ fileId: "abc123", accessRules: { read: [{ type: "public" }] } })Default for inline uploads if accessRules is omitted: uploader and app owner can read; nobody else. The create operation is project-controlled and ignored if passed at upload.
has_role on files: legacy format only
For files, the name-based { type: "has_role", role: "..." } shape only checks the legacy profile roles list — unlike tables, functions, and email, it does not also match the platform role field. Use roleId-based rules for files to avoid surprises.
Function Access Rules
Backend functions have a single operation: execute. Set who can call each function.
// Anyone can call this function
execute: [{ type: "public" }]
// Only logged-in admins can call this function
execute: [{ type: "has_role", role: "admin" }]
// No rules = only the app owner can call it (even logged-in members get "No access rules configured")
execute: []function rules run even for anonymous (logged-out) callers — the gate receives a nullable user, so it can express "public, but only if this custom check passes". has_entitlement gates a function behind a paid-access key:
// Only callers with a live "pro" entitlement can call this function
execute: [{ type: "has_entitlement", key: "pro" }]When Rule Changes Take Effect
Access rules are data, not code — changing them doesn't require a re-publish for most surfaces. Here's the precise model:
| Surface | When changes apply |
|---|---|
| Tables, files, backend functions, flows | Immediately — rules evaluated on every API request |
| Page reads at the edge (Worker) | Immediately — rules pushed to KV on every save |
| In-bundle page guard (client-side) | On next publish — the guard's rule list is baked into the JS |
Practical implication
Tightening a rule (e.g., adding a role requirement) takes effect immediately at the edge — users hit the new gate right away.
Relaxing a rule (e.g., switching protected → public) lets requests through at the edge instantly, but the bundle's in-app guard still has the old strict rule until the next publish. Re-publish to fully relax.
How Rules Are Evaluated
App owner always has access
The person who created the app bypasses all access rules automatically.
A flat rule array uses OR logic
If any rule matches, access is granted. Example: [creator, has_role:admin] means the creator OR any admin can access. For AND / NOT / explicit-deny logic, use a condition group — see Composition & Operators.
No rules = deny
An empty rule array means nobody (except the app owner) can perform that operation.
Creator tracking
When a user creates a row or uploads a file, their ID is saved as creatorId. The creator rule checks this field.
Common Patterns
Public read, authenticated write
A product catalog anyone can browse, but only logged-in users can add to.
read: [{ type: "public" }]
create: [{ type: "authenticated" }]Users manage their own data
Users can only see and edit rows they created.
read: [{ type: "creator" }]
create: [{ type: "authenticated" }]
update: [{ type: "creator" }]
delete: [{ type: "creator" }]Department-scoped data
Users can only see rows where the row's "Department" field matches their profile's department.
read: [{ type: "field_match", rowField: "Department", userField: "department" }]Premium content
Only users with a "premium" subscription tier can access.
read: [{ type: "user_property", field: "subscriptionTier", value: "premium" }]