Skip to content

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.

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.

vski/default/collections.yaml
version: 1
database: 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: null

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"

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"

VSKI provides special variables for accessing request context:

The ID of the authenticated user (if authenticated):

rules:
list: "userId = @request.auth.id"

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"

The data being created or updated:

rules:
create: "category != 'restricted'"
update: "status IN ('draft', 'published')"
# = != > < >= <=
rules:
list: "views > 100"
view: "price <= 50"
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"
rules:
list: "title ~ 'react'" # contains
view: "email ^ 'admin@'" # starts with
create: "domain $ '@example.com'" # ends with
rules:
list: "deletedAt = null"
view: "deletedAt != null"

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: null
Terminal window
vski plan # ~ collection default/posts: rules update
vski apply

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"

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 deleted

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"

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'"

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".

"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 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.

rules:
list: "published = true OR authorId = @request.auth.id"
view: "published = true OR authorId = @request.auth.id"
rules:
list: "teamId IN (SELECT teamId FROM team_members WHERE userId = @request.auth.id)"
rules:
list: "publishDate <= datetime('now') AND (expiryDate IS NULL OR expiryDate > datetime('now'))"
rules:
create: "@request.auth.role IN ('admin', 'editor')"
# Published posts OR own drafts that aren't deleted
rules:
list: "published = true OR (authorId = @request.auth.id AND published = false AND deletedAt IS NULL)"
# User must be a member of the project
rules:
list: "projectId IN (SELECT projectId FROM project_members WHERE userId = @request.auth.id)"
# Only specific users can trigger workflows
rules:
execute: "@request.auth.id IN (SELECT userId FROM workflow_permissions WHERE workflowName = 'my-workflow')"
  1. Default to deny: if in doubt, restrict access rather than allow it
  2. Use specific rules: be explicit about who can do what
  3. Test thoroughly: test rules with different user roles and scenarios
  4. Document your rules: keep clear documentation of what each rule does
  5. Use owner-based patterns: userId = @request.auth.id is a common, safe pattern
  6. Consider soft delete: use deletedAt = null to filter instead of actually deleting
  7. Require authentication: use @request.auth.id IS NOT NULL where "*" would be too open
  8. Validate on create: use create to validate data on creation
  9. Review regularly: periodically review and update rules
  10. Admin bypass: remember that admins bypass all rules
Terminal window
vski collections show posts
result = 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"])
from vski import VskiClient, VskiError
# Test as a regular collection user
user_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 works
admin_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)

Rules are managed through the collections API (used by vski apply via the migration system):

MethodEndpointDescription
POST/api/collectionsCreate collection with rules
PATCH/api/collections/:nameUpdate collection rules
GET/api/collectionsList 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
}