Skip to main content
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.
const authDataRef: Proxy
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

  1. Use ANYONE_CAN for public data: Don’t write complex rules when simple unrestricted access is appropriate
  2. Keep rules simple: Complex permission logic should be in your application code, not permission rules
  3. Test permissions thoroughly: Write tests that verify users can only access data they should
  4. Use cell-level permissions sparingly: They add complexity; prefer row-level permissions when possible
  5. Consider performance: Complex permission rules can impact query performance
  6. Migrate to Mutators: For new projects, use Mutators for all write operations

Build docs developers (and LLMs) love