The permissions API (definePermissions, ANYONE_CAN_DO_ANYTHING, and row-level insert/update/delete permissions) is deprecated. Use Mutators for write operations instead.
Overview
Zero’s permissions system allows you to define fine-grained access control rules for your data. While write permissions are deprecated in favor of Mutators, read permissions (select) remain the recommended way to control data visibility.
Constants
ANYONE_CAN
Allows unrestricted access to an operation.
const ANYONE_CAN: PermissionRule[]
Example:
import { ANYONE_CAN } from '@rocicorp/zero';
const permissions = {
user: {
row: {
select: ANYONE_CAN,
},
},
};
NOBODY_CAN
Denies all access to an operation.
const NOBODY_CAN: PermissionRule[]
Example:
import { NOBODY_CAN } from '@rocicorp/zero';
const permissions = {
sensitiveData: {
row: {
select: NOBODY_CAN,
},
},
};
ANYONE_CAN_DO_ANYTHING (Deprecated)
A convenience constant allowing all operations on rows.
const ANYONE_CAN_DO_ANYTHING: {
row: {
select: PermissionRule[];
insert: PermissionRule[];
update: {
preMutation: PermissionRule[];
postMutation: PermissionRule[];
};
delete: PermissionRule[];
};
}
Deprecated. Use ANYONE_CAN for select permissions and Mutators for write operations.
Functions
definePermissions() (Deprecated)
Defines permissions for your schema.
function definePermissions<TAuthDataShape, TSchema extends Schema>(
schema: TSchema,
definer: () => Promise<PermissionsConfig<TAuthDataShape, TSchema>> | PermissionsConfig<TAuthDataShape, TSchema>
): Promise<CompiledPermissionsConfig | undefined>
Parameters:
schema - Your Zero schema
definer - Function returning permission configuration
Returns: Compiled permissions configuration
Deprecated. Use defineQueries for read permissions and defineMutators for write operations.
Permission Rules
PermissionRule
A function that returns a condition determining access.
type PermissionRule<TAuthDataShape, TSchema, TTable> = (
authData: TAuthDataShape,
eb: ExpressionBuilder<TTable, TSchema>
) => Condition
Parameters:
authData - Authentication data (e.g., from JWT)
eb - Expression builder for constructing conditions
Returns: A condition that must be true for access to be granted
Example:
const permissions = {
issue: {
row: {
// Users can only see public issues or issues they created
select: [
(authData, eb) => eb.or(
eb.cmp('visibility', '=', 'public'),
eb.cmp('creatorID', '=', authData.sub)
),
],
},
},
};
Permission Configuration
Row-Level Permissions
Control access to entire rows.
type AssetPermissions<TAuthDataShape, TSchema, TTable> = {
select?: PermissionRule[];
insert?: PermissionRule[]; // Deprecated
update?: { // Deprecated
preMutation?: PermissionRule[];
postMutation?: PermissionRule[];
};
delete?: PermissionRule[]; // Deprecated
}
Example:
interface AuthData {
sub: string; // User ID
role: string;
}
const permissions: PermissionsConfig<AuthData, typeof schema> = {
issue: {
row: {
// Users can see public issues or their own issues
select: [
(authData, eb) => eb.or(
eb.cmp('visibility', '=', 'public'),
eb.cmp('creatorID', '=', authData.sub)
),
],
},
},
user: {
row: {
// Anyone can read user profiles
select: ANYONE_CAN,
},
},
};
Cell-Level Permissions
Control access to specific columns.
type CellPermissions = {
[columnName: string]: {
select?: PermissionRule[];
insert?: PermissionRule[]; // Deprecated
update?: { // Deprecated
preMutation?: PermissionRule[];
postMutation?: PermissionRule[];
};
};
}
Example:
const permissions: PermissionsConfig<AuthData, typeof schema> = {
user: {
row: {
select: ANYONE_CAN, // Anyone can see users exist
},
cell: {
// Only the user themselves can see their email
email: {
select: [
(authData, eb) => eb.cmp('id', '=', authData.sub),
],
},
// Only admins can see roles
role: {
select: [
(authData, eb) => eb.and(), // Always evaluate to check authData.role
],
},
},
},
};
Auth Data Reference
authDataRef
Reference to the authenticated user’s data in permission rules.
Example:
import { authDataRef } from '@rocicorp/zero';
const permissions = {
issue: {
row: {
select: [
(authData, eb) => eb.cmp('creatorID', '=', authDataRef.sub),
],
},
},
};
The authDataRef is a proxy that tracks field accesses for optimization. Accessing authDataRef.sub in a rule makes that JWT field available to the permission evaluator.
Expression Builder
The ExpressionBuilder (parameter eb) provides methods for constructing conditions:
Comparison
eb.cmp(field: string, operator: '=' | '!=' | '>' | '<' | '>=' | '<=', value: any)
Example:
eb.cmp('creatorID', '=', authData.sub)
eb.cmp('age', '>=', 18)
Logical Operators
eb.and(...conditions: Condition[])
eb.or(...conditions: Condition[])
Example:
eb.and(
eb.cmp('visibility', '=', 'public'),
eb.cmp('status', '=', 'active')
)
eb.or(
eb.cmp('creatorID', '=', authData.sub),
eb.cmp('assigneeID', '=', authData.sub)
)
Permission Arrays
Permissions are defined as arrays to support multiple rules. A user has access if any rule in the array passes.
select: [
// Rule 1: User is the creator
(authData, eb) => eb.cmp('creatorID', '=', authData.sub),
// OR Rule 2: Issue is public
(authData, eb) => eb.cmp('visibility', '=', 'public'),
]
Multiple rules in an array form an OR condition. If you need AND logic, use eb.and() within a single rule.
Complete Example
Here’s a complete permissions configuration:
import {
definePermissions,
ANYONE_CAN,
NOBODY_CAN,
type PermissionsConfig,
} from '@rocicorp/zero';
import { schema } from './schema';
interface AuthData {
sub: string; // User ID
role: 'admin' | 'member' | 'guest';
email: string;
}
export const permissions = await definePermissions<AuthData, typeof schema>(
schema,
() => ({
// Users table
user: {
row: {
select: ANYONE_CAN,
},
cell: {
email: {
select: [
// Users can only see their own email
(authData, eb) => eb.cmp('id', '=', authData.sub),
],
},
},
},
// Issues table
issue: {
row: {
select: [
// Public issues are visible to all
(authData, eb) => eb.cmp('visibility', '=', 'public'),
// Users can see issues they created
(authData, eb) => eb.cmp('creatorID', '=', authData.sub),
// Users can see issues assigned to them
(authData, eb) => eb.cmp('assigneeID', '=', authData.sub),
],
},
},
// Comments table
comment: {
row: {
select: ANYONE_CAN, // Rely on issue permissions
},
},
// Admin-only table
auditLog: {
row: {
select: [
(authData, eb) => {
if (authData.role !== 'admin') {
return eb.and(); // Admins only - return empty condition
}
return eb.and(); // Allow all for admins
},
],
},
},
})
);
Migration Guide
From Deprecated Permissions to Mutators
If you’re currently using deprecated write permissions, migrate to Mutators:
Before (Deprecated):
const permissions = {
issue: {
row: {
insert: [
(authData, eb) => eb.cmp('creatorID', '=', authData.sub),
],
update: {
preMutation: [
(authData, eb) => eb.cmp('creatorID', '=', authData.sub),
],
},
},
},
};
After (Recommended):
import { defineMutators } from '@rocicorp/zero';
export const mutators = defineMutators(schema, {
createIssue: async (tx, { title, description }) => {
const authData = tx.authData;
await tx.issue.insert({
id: generateID(),
title,
description,
creatorID: authData.sub,
created: Date.now(),
});
},
updateIssue: async (tx, { id, title }) => {
const issue = await tx.issue.get(id);
if (issue.creatorID !== tx.authData.sub) {
throw new Error('Not authorized');
}
await tx.issue.update({ id, title });
},
});
See Writing Data for more details.
Best Practices
- Use ANYONE_CAN for public data: Don’t write complex rules when simple unrestricted access is appropriate
- Keep rules simple: Complex permission logic should be in your application code, not permission rules
- Test permissions thoroughly: Write tests that verify users can only access data they should
- Use cell-level permissions sparingly: They add complexity; prefer row-level permissions when possible
- Consider performance: Complex permission rules can impact query performance
- Migrate to Mutators: For new projects, use Mutators for all write operations