2 Rules and Access Control
Anton Nesterov edited this page 2026-02-22 22:54:39 +01:00

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

  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. Rule for unauthenticated - Use @request.auth.id IS NOT NULL to require authentication
  8. Validate on create - Use createRule to validate data on creation
  9. Review regularly - Periodically review and update rules
  10. 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,
  ...
}