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
Rules are SQL WHERE clause expressions that are applied as filters to database queries. Each collection can have up to six different rule types:
- listRule - Controls who can list records (affects getList and search)
- viewRule - Controls who can view individual records (affects getOne)
- createRule - Controls who can create new records (affects create)
- updateRule - Controls who can update existing records (affects update and bulkUpdate)
- deleteRule - Controls who can delete records (affects delete and bulkDelete)
- executeRule - Controls who can execute workflow triggers on the collection
Creating Collections with Rules
Basic Example: Admin-Only Collection
await client.settings.collections.create({
name: "admin_settings",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "key", type: "text", required: true },
{ id: crypto.randomUUID(), name: "value", type: "text" },
],
listRule: "1=0", // Always false - no one can list
viewRule: "1=0", // Always false - no one can view
createRule: "1=0", // Always false - no one can create
updateRule: "1=0", // Always false - no one can update
deleteRule: "1=0", // Always false - no one can delete
});
Note: Admin users bypass all rules and can access all collections and records.
Owner-Based Rules
Allow users to only access their own records:
await client.settings.collections.create({
name: "posts",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "title", type: "text", required: true },
{ id: crypto.randomUUID(), name: "content", type: "text" },
{ id: crypto.randomUUID(), name: "authorId", type: "text", required: true },
],
listRule: "authorId = @request.auth.id",
viewRule: "authorId = @request.auth.id",
createRule: "authorId = @request.auth.id",
updateRule: "authorId = @request.auth.id",
deleteRule: "authorId = @request.auth.id",
});
Public Read, Private Write
Common pattern for blog posts, comments, etc.:
await client.settings.collections.create({
name: "comments",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "text", type: "text", required: true },
{ id: crypto.randomUUID(), name: "authorId", type: "text", required: true },
{ id: crypto.randomUUID(), name: "postId", type: "text", required: true },
],
listRule: "1=1", // Everyone can list
viewRule: "1=1", // Everyone can view
createRule: "authorId = @request.auth.id", // Only create own
updateRule: "authorId = @request.auth.id", // Only update own
deleteRule: "authorId = @request.auth.id", // Only delete own
});
Rule Variables
VSKI provides special variables for accessing request context:
@request.auth.id
The ID of the authenticated user (if authenticated):
// Only user's own records
listRule: "userId = @request.auth.id";
@request.auth.role
The role of the authenticated user (requires role field in user collection):
// Only admins can create
createRule: "@request.auth.role = 'admin'";
@request.data
The data being created or updated:
// Prevent creating certain types
createRule: "category != 'restricted'";
// Validate during creation
createRule: "status IN ('draft', 'published')";
Rule Operators
Comparison Operators
// Equals
listRule: "userId = @request.auth.id";
// Not equals
listRule: "status != 'deleted'";
// Greater than
listRule: "views > 100";
// Less than
listRule: "price < 50";
// Greater than or equal
listRule: "rating >= 4";
// Less than or equal
listRule: "age <= 18";
Logical Operators
// AND
listRule: "published = true && userId = @request.auth.id";
// OR
listRule: "status = 'active' || status = 'pending'";
// NOT
listRule: "!(status = 'deleted' || status = 'archived')";
// Grouping with parentheses
listRule: "(status = 'active' || status = 'pending') && userId = @request.auth.id";
String Operators
// Contains
listRule: "title ~ 'react'";
// Starts with
listRule: "email ^ 'admin@'";
// Ends with
listRule: "domain $ '@example.com'";
Null Checks
// Is null
listRule: "deletedAt = null";
// Is not null
listRule: "deletedAt != null";
Updating Rules on Existing Collections
You can update rules by updating the collection:
// Make collection read-only
await client.settings.collections.update("posts", {
createRule: "1=0",
updateRule: "1=0",
deleteRule: "1=0",
});
Note: When updating a collection, you must provide all rule properties you want to set, even if they were previously defined.
Practical Examples
Blog System
Public can read published posts, authors can manage their own posts:
await client.settings.collections.create({
name: "posts",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "title", type: "text", required: true },
{ id: crypto.randomUUID(), name: "content", type: "text" },
{ id: crypto.randomUUID(), name: "published", type: "bool" },
{ id: crypto.randomUUID(), name: "authorId", type: "text", required: true },
],
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",
});
E-commerce System
Anyone can view products, customers manage their own orders:
// Products collection
await client.settings.collections.create({
name: "products",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "name", type: "text", required: true },
{ id: crypto.randomUUID(), name: "price", type: "number", required: true },
{ id: crypto.randomUUID(), name: "stock", type: "number", required: true },
],
listRule: "published = true && stock > 0",
viewRule: "published = true",
createRule: "1=0", // Only admin can create products
updateRule: "1=0",
deleteRule: "1=0",
});
// Orders collection
await client.settings.collections.create({
name: "orders",
type: "base",
fields: [
{
id: crypto.randomUUID(),
name: "customerId",
type: "text",
required: true,
},
{ id: crypto.randomUUID(), name: "total", type: "number", required: true },
{ id: crypto.randomUUID(), name: "status", type: "text", required: true },
],
listRule: "customerId = @request.auth.id",
viewRule: "customerId = @request.auth.id",
createRule: "customerId = @request.auth.id",
updateRule: "customerId = @request.auth.id",
deleteRule: "1=0", // Can't delete orders
});
Collaborative Documents
Document owners can manage, collaborators can view:
await client.settings.collections.create({
name: "documents",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "title", type: "text", required: true },
{ id: crypto.randomUUID(), name: "content", type: "text" },
{ id: crypto.randomUUID(), name: "ownerId", type: "text", required: true },
{ id: crypto.randomUUID(), name: "shared", type: "bool" },
],
listRule: "ownerId = @request.auth.id || shared = true",
viewRule: "ownerId = @request.auth.id || shared = true",
createRule: "ownerId = @request.auth.id",
updateRule: "ownerId = @request.auth.id",
deleteRule: "ownerId = @request.auth.id",
});
Soft Delete Pattern
Exclude soft-deleted records from queries:
await client.settings.collections.create({
name: "comments",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "text", type: "text", required: true },
{ id: crypto.randomUUID(), name: "deletedAt", type: "date" },
],
listRule: "deletedAt = null",
viewRule: "deletedAt = null",
createRule: "1=1",
updateRule: "deletedAt = null || @request.auth.role = 'admin'",
deleteRule: "deletedAt = null || @request.auth.role = 'admin'",
});
Rule Evaluation Logic
Empty Rules
If a rule property is not set or is empty (null, "", or omitted), the
operation is allowed for all authenticated users:
// No rules - all authenticated users can access
await client.settings.collections.create({
name: "public_posts",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "title", type: "text" },
],
// No rule properties set = open access
});
False Rules
Use "1=0" to completely disable an operation:
await client.settings.collections.create({
name: "system_config",
type: "base",
fields: [
{ id: crypto.randomUUID(), name: "key", type: "text" },
{ id: crypto.randomUUID(), name: "value", type: "text" },
],
listRule: "1=0", // No one can list (except admin)
viewRule: "1=0", // No one can view (except admin)
createRule: "1=0", // No one can create (except admin)
updateRule: "1=0", // No one can update (except admin)
deleteRule: "1=0", // No one can delete (except admin)
});
Admin Bypass
Admin users (authenticated via client.admins.authWithPassword()) bypass all
rules. They have full access to all collections and can perform all operations
regardless of rules.
Common Patterns
Published vs Draft
// Public can only see published, authors can see their drafts
listRule: "published = true || authorId = @request.auth.id";
viewRule: "published = true || authorId = @request.auth.id";
Team-Based Access
// Access if user is a team member
listRule: "teamId IN (SELECT teamId FROM team_members WHERE userId = @request.auth.id)";
Time-Based Access
// Only access content within date range
listRule: "publishDate <= datetime('now') AND (expiryDate IS NULL OR expiryDate > datetime('now'))";
Role-Based Access
// Only specific roles
createRule: "@request.auth.role IN ('admin', 'editor')";
Complex Rule Examples
Multi-Condition Rules
// Published posts OR own drafts that aren't deleted
listRule: "(published = true) || (authorId = @request.auth.id && published = false && deletedAt IS NULL)";
Subquery for Access Control
// User must be a member of the project
listRule: "projectId IN (SELECT projectId FROM project_members WHERE userId = @request.auth.id)";
Workflow Execution Control
// Only specific users can trigger workflows
executeRule: "@request.auth.id IN (SELECT userId FROM workflow_permissions WHERE workflowName = 'my-workflow')";
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 - Rule for unauthenticated - Use
@request.auth.id IS NOT NULLto require authentication - Validate on create - Use
createRuleto validate data on creation - Review regularly - Periodically review and update rules
- Admin bypass - Remember that admins bypass all rules
Debugging Rules
Check Rules on Collection
const collection = await client.settings.collections.getList();
const posts = collection.items.find((c) => c.name === "posts");
console.log("List Rule:", posts.listRule);
console.log("View Rule:", posts.viewRule);
console.log("Create Rule:", posts.createRule);
console.log("Update Rule:", posts.updateRule);
console.log("Delete Rule:", posts.deleteRule);
Test with Different Users
// Test as regular user
const userClient = new VskiClient("http://localhost:3000");
await userClient.auth.login("user@example.com", "password");
try {
await userClient.collection("posts").getList(1, 10);
console.log("User can list posts");
} catch (error) {
console.log("User cannot list posts:", error.message);
}
// Test as admin
const adminClient = new VskiClient("http://localhost:3000");
await adminClient.admins.authWithPassword(
"admin@rocketbase.dev",
"password123",
);
await adminClient.collection("posts").getList(1, 10); // Always works
API Endpoints
Rules are managed through the collections API:
| 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,
...
}