Guides
GuideAccess Control

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.

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.

TypeWho it matchesExample use case
publicAnyone, no login requiredPublic product catalog
authenticatedAny logged-in userMembers-only content
creatorThe user who created the row/fileUsers edit own profile
has_roleUsers with a specific roleAdmin-only actions
field_matchRow field equals user's profile fieldDepartment-scoped data
user_propertyUser's profile field has specific valuePremium subscribers only
emailSpecific email addressSingle-user access
has_entitlementUsers with a live paid-access entitlement keyGate a feature behind a purchase
functionCustom logic — a backend function returns true/false (runs even for anonymous, logged-out users)Bespoke eligibility checks
relationshipAccess 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:

OperationWhat it controls
readWho can view rows (getRows, getRow, searchRows)
createWho can add new rows
updateWho can modify existing rows
deleteWho 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.

OperationWhat it controls
createWho can upload files
readWho can view and download files
deleteWho 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:

SurfaceWhen changes apply
Tables, files, backend functions, flowsImmediately — 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" }]