Rules and Access Control
VSKI’s rule engine provides powerful, flexible access control.
Rules are defined directly on collections and control who can perform which operations on your data.
Understanding Rules
Section titled “Understanding Rules”Rules are SQL WHERE clause expressions that are applied as filters to database queries. Each collection can have up to six different rule types:
- list - Controls who can list records (affects get_list and search)
- view - Controls who can view individual records (affects get_one)
- create - Controls who can create new records (affects create)
- update - Controls who can update existing records (affects update and bulk_update)
- delete - Controls who can delete records (affects delete and bulk_delete)
- execute - Controls who can execute workflow triggers on the collection
Rules are declared in the IaC YAML (see IaC) and applied with
vski apply. Three values have special meaning:
- missing or
null: the operation is closed; only admins can perform it "*": any caller (this is the only way to allow unauthenticated access)- an expression: evaluated per request, e.g.
"authorId = @request.auth.id"
Admin users bypass all rules and can access all collections and records.
Collections with Rules
Section titled “Collections with Rules”Basic Example: Admin-Only Collection
Section titled “Basic Example: Admin-Only Collection”version: 1database: default
collections: admin_settings: type: base fields: - {name: key, type: text, required: true} - {name: value, type: text} rules: list: null # null = closed (admin only); "1=0" is equivalent view: null create: null update: null delete: nullOwner-Based Rules
Section titled “Owner-Based Rules”Allow users to only access their own records:
collections: posts: type: base fields: - {name: title, type: text, required: true} - {name: content, type: text} - {name: authorId, type: text, required: true} rules: list: "authorId = @request.auth.id" view: "authorId = @request.auth.id" create: "authorId = @request.auth.id" update: "authorId = @request.auth.id" delete: "authorId = @request.auth.id"Public Read, Private Write
Section titled “Public Read, Private Write”Common pattern for blog posts, comments, etc.:
collections: comments: type: base fields: - {name: text, type: text, required: true} - {name: authorId, type: text, required: true} - {name: postId, type: text, required: true} rules: list: "*" view: "*" create: "authorId = @request.auth.id" update: "authorId = @request.auth.id" delete: "authorId = @request.auth.id"Rule Variables
Section titled “Rule Variables”VSKI provides special variables for accessing request context:
@request.auth.id
Section titled “@request.auth.id”The ID of the authenticated user (if authenticated):
rules: list: "userId = @request.auth.id"@request.auth.role
Section titled “@request.auth.role”The role of the authenticated user (requires role field in user collection):
rules: create: "@request.auth.role = 'admin'"The multi-value form checks membership in the user’s roles:
rules: list: "'admin' in @request.auth.roles"@request.data
Section titled “@request.data”The data being created or updated:
rules: create: "category != 'restricted'" update: "status IN ('draft', 'published')"Rule Operators
Section titled “Rule Operators”Comparison Operators
Section titled “Comparison Operators”# = != > < >= <=rules: list: "views > 100" view: "price <= 50"Logical Operators
Section titled “Logical Operators”rules: list: "published = true AND userId = @request.auth.id" view: "status = 'active' OR status = 'pending'" update: "NOT (status = 'deleted' OR status = 'archived')" delete: "(status = 'active' OR status = 'pending') AND userId = @request.auth.id"String Operators
Section titled “String Operators”rules: list: "title ~ 'react'" # contains view: "email ^ 'admin@'" # starts with create: "domain $ '@example.com'" # ends withNull Checks
Section titled “Null Checks”rules: list: "deletedAt = null" view: "deletedAt != null"Updating Rules on Existing Collections
Section titled “Updating Rules on Existing Collections”Edit the YAML and apply: the rules block is authoritative, so a vski apply
turns the diff into an update_rules migration step.
collections: posts: rules: create: null # make the collection read-only update: null delete: nullvski plan # ~ collection default/posts: rules updatevski applyPractical Examples
Section titled “Practical Examples”Blog System
Section titled “Blog System”Public can read published posts, authors can manage their own posts:
collections: posts: type: base fields: - {name: title, type: text, required: true} - {name: content, type: text} - {name: published, type: bool} - {name: authorId, type: text, required: true} rules: list: "published = true" view: "published = true OR authorId = @request.auth.id" create: "authorId = @request.auth.id" update: "authorId = @request.auth.id" delete: "authorId = @request.auth.id"E-commerce System
Section titled “E-commerce System”Anyone can view products, customers manage their own orders:
collections: products: type: base fields: - {name: name, type: text, required: true} - {name: price, type: number, required: true} - {name: stock, type: number, required: true} rules: list: "published = true AND stock > 0" view: "published = true" create: null update: null delete: null
orders: type: base fields: - {name: customerId, type: text, required: true} - {name: total, type: number, required: true} - {name: status, type: text, required: true} rules: list: "customerId = @request.auth.id" view: "customerId = @request.auth.id" create: "customerId = @request.auth.id" update: "customerId = @request.auth.id" delete: null # orders are never deletedCollaborative Documents
Section titled “Collaborative Documents”Document owners can manage, collaborators can view:
collections: documents: type: base fields: - {name: title, type: text, required: true} - {name: content, type: text} - {name: ownerId, type: text, required: true} - {name: shared, type: bool} rules: list: "ownerId = @request.auth.id OR shared = true" view: "ownerId = @request.auth.id OR shared = true" create: "ownerId = @request.auth.id" update: "ownerId = @request.auth.id" delete: "ownerId = @request.auth.id"Soft Delete Pattern
Section titled “Soft Delete Pattern”Exclude soft-deleted records from queries:
collections: comments: type: base fields: - {name: text, type: text, required: true} - {name: deletedAt, type: date} rules: list: "deletedAt = null" view: "deletedAt = null" create: "*" update: "deletedAt = null OR @request.auth.role = 'admin'" delete: "deletedAt = null OR @request.auth.role = 'admin'"Rule Evaluation Logic
Section titled “Rule Evaluation Logic”Empty Rules
Section titled “Empty Rules”An unset rule (or null, or "") closes the operation: the compiled filter
becomes 1=0, so no regular user can match it. To open an operation to every
caller, use "*"; to open it to every authenticated user, use
"@request.auth.id IS NOT NULL".
False Rules
Section titled “False Rules”"1=0" behaves identically to an unset rule, spelled explicitly:
collections: system_config: type: base fields: - {name: key, type: text} - {name: value, type: text} rules: list: "1=0" view: "1=0" create: "1=0" update: "1=0" delete: "1=0"Admin Bypass
Section titled “Admin Bypass”Admin users (authenticated via admins.auth_with_password, or API keys with
the admin role) bypass all rules. They have full access to all collections
and can perform all operations regardless of rules.
Common Patterns
Section titled “Common Patterns”Published vs Draft
Section titled “Published vs Draft”rules: list: "published = true OR authorId = @request.auth.id" view: "published = true OR authorId = @request.auth.id"Team-Based Access
Section titled “Team-Based Access”rules: list: "teamId IN (SELECT teamId FROM team_members WHERE userId = @request.auth.id)"Time-Based Access
Section titled “Time-Based Access”rules: list: "publishDate <= datetime('now') AND (expiryDate IS NULL OR expiryDate > datetime('now'))"Role-Based Access
Section titled “Role-Based Access”rules: create: "@request.auth.role IN ('admin', 'editor')"Complex Rule Examples
Section titled “Complex Rule Examples”Multi-Condition Rules
Section titled “Multi-Condition Rules”# Published posts OR own drafts that aren't deletedrules: list: "published = true OR (authorId = @request.auth.id AND published = false AND deletedAt IS NULL)"Subquery for Access Control
Section titled “Subquery for Access Control”# User must be a member of the projectrules: list: "projectId IN (SELECT projectId FROM project_members WHERE userId = @request.auth.id)"Workflow Execution Control
Section titled “Workflow Execution Control”# Only specific users can trigger workflowsrules: execute: "@request.auth.id IN (SELECT userId FROM workflow_permissions WHERE workflowName = 'my-workflow')"Security Best Practices
Section titled “Security Best Practices”- Default to deny: if in doubt, restrict access rather than allow it
- Use specific rules: be explicit about who can do what
- Test thoroughly: test rules with different user roles and scenarios
- Document your rules: keep clear documentation of what each rule does
- Use owner-based patterns:
userId = @request.auth.idis a common, safe pattern - Consider soft delete: use
deletedAt = nullto filter instead of actually deleting - Require authentication: use
@request.auth.id IS NOT NULLwhere"*"would be too open - Validate on create: use
createto validate data on creation - Review regularly: periodically review and update rules
- Admin bypass: remember that admins bypass all rules
Debugging Rules
Section titled “Debugging Rules”Check Rules on a Collection
Section titled “Check Rules on a Collection”vski collections show postsresult = await client.settings.collections.get_list()posts = next(c for c in result["items"] if c["name"] == "posts")print(posts["listRule"], posts["viewRule"], posts["createRule"])Test with Different Users
Section titled “Test with Different Users”from vski import VskiClient, VskiError
# Test as a regular collection useruser_client = VskiClient("http://127.0.0.1:3000")await user_client.collection("users").auth_with_password( "user@example.com", "password")try: await user_client.collection("posts").get_list(1, 10) print("user can list posts")except VskiError as e: print(f"user cannot list posts: {e}")
# Test as admin: always worksadmin_client = VskiClient("http://127.0.0.1:3000")res = await admin_client.admins.auth_with_password("admin@rocketbase.dev", "password123")admin_client.set_token(res["token"])await admin_client.collection("posts").get_list(1, 10)API Endpoints
Section titled “API Endpoints”Rules are managed through the collections API (used by vski apply via the
migration system):
| Method | Endpoint | Description |
|---|---|---|
POST | /api/collections | Create collection with rules |
PATCH | /api/collections/:name | Update collection rules |
GET | /api/collections | List collections with rules |
Rule properties are included in the collection object:
{ "id": "collection-id", "name": "posts", "type": "base", "schema": [], "listRule": "published = true", "viewRule": "published = true || authorId = @request.auth.id", "createRule": "authorId = @request.auth.id", "updateRule": "authorId = @request.auth.id", "deleteRule": "authorId = @request.auth.id", "executeRule": null}