Skip to content

Granular Access Control

VSKI provides field-level access control that allows you to control which fields are visible or editable by different user groups. This works in addition to the standard collection-level rules.

Granular access control gives you fine-grained control over:

  • View Rules (fieldRules) - Control which fields users can READ (view)
  • Edit Rules (fieldEditRules) - Control which fields users can WRITE (create/update)
  1. Data Privacy - Hide sensitive fields (like passwords, internal notes) from certain user groups
  2. Data Integrity - Prevent users from modifying critical fields (like status, approval flags)
  3. Flexible Security - Override global rules with field-level restrictions
  4. Group-Based - Different access levels for different user roles

Field-level rules have higher priority than global collection rules:

Collection-level rules:
├─ listRule
├─ viewRule
├─ createRule
├─ updateRule
├─ deleteRule
└─ executeRule
Field-level rules (HIGHER PRIORITY):
├─ fieldRules (VIEW) → Overrides viewRule, listRule
└─ fieldEditRules (EDIT) → Overrides createRule, updateRule

When field rules are defined for a user’s groups, global rules are bypassed.

Field rules live in the collection’s options block in the IaC YAML (see IaC), as a map of group name to allowed fields:

options:
fieldRules:
groupName1: [field1, field2, field3]
groupName2: ["*"] # Wildcard = all fields
"*": [publicField] # Public group applies to all users

The following system fields are always included in view rules and always read-only in edit rules:

  • id - Record identifier
  • created - Creation timestamp
  • updated - Last update timestamp

These fields cannot be removed from field rules.

View rules control READ access - which fields users can see when listing or viewing records.

vski/default/collections.yaml
version: 1
database: default
collections:
user_profiles:
type: base
fields:
- {name: username, type: text, required: true}
- {name: email, type: email, required: true}
- {name: phone, type: text}
- {name: ssn, type: text} # Sensitive!
- {name: notes, type: text} # Internal!
rules:
list: "1=1"
view: "1=1"
create: "1=0" # Admin only
update: "1=0"
delete: "1=0"
options:
# Field-level view rules
fieldRules:
public: [username] # Public can only see username
viewer: [username, email, phone] # Viewers can't see SSN or notes
admin: ["*"] # Admins see everything
Terminal window
vski apply

When a user queries records with field-level restrictions:

# User in "viewer" group
page = await client.collection("user_profiles").get_list(1, 10)
# Result: only username, email, phone, id, created, updated
# SSN and notes are filtered out automatically
# page["items"][0] =>
# {
# "id": "abc123",
# "username": "john_doe",
# "email": "john@example.com",
# "phone": "+1234567890",
# "created": "2026-02-22T10:00:00Z",
# "updated": "2026-02-22T10:00:00Z",
# # ssn and notes are NOT included
# }

If a user has field-level view access, global viewRule and listRule are bypassed:

# Global rule says "no access"
rules:
view: "1=0" # No one can view (except admin)
# But field rules give access
options:
fieldRules:
viewer: [title] # Viewers can see title
# Result: User in "viewer" group CAN view records
# despite global viewRule being "1=0"

Edit rules control WRITE access - which fields users can set when creating or updating records.

collections:
support_tickets:
type: base
fields:
- {name: title, type: text, required: true}
- {name: description, type: text, required: true}
- {name: status, type: text, required: true} # Workflow field
- {name: priority, type: text, required: true} # Workflow field
- {name: resolution, type: text}
rules:
list: "1=1"
view: "1=1"
create: "1=0"
update: "1=0"
delete: "1=0"
options:
# Field-level edit rules
fieldEditRules:
customer: [title, description] # Customers can only set title, description
agent: [title, description, resolution] # Agents can't change status/priority
admin: ["*"] # Admins can edit everything
# User in "customer" group
record = await client.collection("support_tickets").create(
{
"title": "Login issue",
"description": "I can't log in",
"status": "open", # IGNORED: not in customer's edit list
"priority": "high", # IGNORED: not in customer's edit list
"resolution": "", # IGNORED: not in customer's edit list
}
)
# Result: Record created with only allowed fields
# {
# "id": "ticket-123",
# "title": "Login issue",
# "description": "I can't log in",
# "status": "open", # Set by DB default or trigger
# "priority": "normal", # Set by DB default or trigger
# "resolution": None,
# "created": "2026-02-22T10:00:00Z",
# "updated": "2026-02-22T10:00:00Z",
# }
# User in "agent" group
updated = await client.collection("support_tickets").update(
"ticket-123",
{
"title": "Updated title", # ALLOWED
"description": "Updated description", # ALLOWED
"resolution": "Fixed by password reset", # ALLOWED
"status": "closed", # IGNORED: not in agent's edit list
"priority": "low", # IGNORED: not in agent's edit list
},
)
# Result: Only title, description, resolution are updated
# status and priority remain unchanged

If a required field is not in a user’s edit field list, the server logs a warning but allows the operation. The field may still get a value through:

  1. Database default values
  2. Trigger hooks
  3. Migration defaults

Example:

# Field "status" is required
fields:
- {name: status, type: text, required: true}
# But customer group can't edit it
options:
fieldEditRules:
customer: [title, description] # status NOT included
# Customer creates record - warning logged but allowed
await client.collection("tickets").create(
{
"title": "Issue",
"description": "Details",
# status not provided, but DB has default "open"
}
)
# Server logs: "Creating record with required fields not in allowed edit fields: status"
# But record is created successfully with status = "open" (default)

If a user has field-level edit access, global createRule and updateRule are bypassed:

# Global rule says "no access"
rules:
create: "1=0" # No one can create (except admin)
update: "1=0" # No one can update (except admin)
# But field rules give access
options:
fieldEditRules:
creator: [title] # Creators can create/edit title
# Result: User in "creator" group CAN create/update records
# despite global createRule/updateRule being "1=0"

The * character has special meanings:

The "*" group represents all users, including unauthenticated (guest) users:

options:
fieldRules:
"*": [title, description] # Everyone can see these
admin: ["*"] # Admins see everything

A field list containing only ["*"] means all fields are allowed:

options:
fieldEditRules:
editor: ["*"] # Editors can edit every field
viewer: [title, description] # Viewers restricted

Users can belong to multiple groups. Field rules are combined using union logic:

options:
fieldRules:
"*": [title] # Public - everyone gets this
team_member: [description] # Team members get this
admin: [notes, internal] # Admins get this
# User in "team_member" group:
# Can see: title (from *) + description (from team_member)
# Cannot see: notes, internal (only admin)

Public can see name/title, HR sees salary, managers see all:

collections:
employees:
type: base
fields:
- {name: name, type: text, required: true}
- {name: title, type: text, required: true}
- {name: department, type: text, required: true}
- {name: salary, type: number} # Sensitive!
- {name: performance_notes, type: text} # Internal!
rules:
list: "1=0"
view: "1=0"
create: "1=0"
update: "1=0"
delete: "1=0"
options:
fieldRules:
public: [name, title] # Anyone can see name/title
hr: [name, title, department, salary] # HR can see salary
manager: ["*"] # Managers see everything
fieldEditRules:
hr: [name, title, department, salary] # HR can edit these
manager: ["*"] # Managers can edit everything

Customers can read limited data, support can read all:

collections:
orders:
type: base
fields:
- {name: customerId, type: text, required: true}
- {name: items, type: json, required: true}
- {name: total, type: number, required: true}
- {name: status, type: text, required: true}
- {name: internal_notes, type: text} # Internal!
rules:
list: "1=0"
view: "1=0"
create: "1=0"
update: "1=0"
delete: "1=0"
options:
fieldRules:
"*": [customerId, items, total, status] # Public can't see notes
support: ["*"] # Support sees everything
fieldEditRules:
customer: [] # Customers can't create/edit (only view)
support: [status, internal_notes] # Support can update these

Editors can create content, editors can edit content, admins manage everything:

collections:
articles:
type: base
fields:
- {name: title, type: text, required: true}
- {name: content, type: text, required: true}
- {name: published, type: bool, required: true}
- {name: author, type: text, required: true}
- {name: featured, type: bool} # Admin only!
rules:
list: "published = true"
view: "published = true || author = @request.auth.id"
create: null
update: null
delete: "1=0"
options:
fieldEditRules:
author: [title, content, published] # Authors can't change author or featured
editor: [title, content, published] # Same for editors
admin: ["*"] # Admins control everything

Field-level permissions are configured in the Collection Settings page:

  1. Navigate to Settings → Collections
  2. Click on a collection to edit
  3. Go to Access tab
  4. Scroll to Field-Level Permissions section

The field permissions UI has two separate sections:

Controls which fields users can READ:

  • Shows which fields are visible for each group
  • System fields (id, created, updated) are always included
  • Badge: “Controls READ visibility”

Controls which fields users can CREATE/UPDATE:

  • Shows which fields are editable for each group
  • System fields are read-only
  • Warnings when required fields are missing from a group’s allowed list
  • Badge: “Controls CREATE/UPDATE access”

For each group (or * for public):

  1. Add Group Rule - Create a new permission group
  2. Toggle Fields - Click field buttons to add/remove from allowed list
  3. All Fields (*) - Click to allow all fields for this group
  4. Remove Group - Delete a permission group

When a required field is not in a group’s edit rules:

⚠ Required fields not editable: field1, field2

Users in this group won’t be able to set these fields during create/update.

The record can still be created if the field has a default value or is set via triggers.

Field rules are included in the collection configuration (see CollectionConfig in the SDK):

class CollectionConfig(TypedDict, total=False):
# ... other properties
fieldRules: dict[str, list[str]] # View rules
fieldEditRules: dict[str, list[str]] # Edit rules

Declared in the IaC YAML like any other collection setting:

collections:
my_collection:
type: base
fields:
- {name: title, type: text}
- {name: description, type: text}
options:
fieldRules:
viewer: [title, description]
fieldEditRules:
viewer: [title]

Edit the options block in the YAML and apply; the block is authoritative:

collections:
my_collection:
options:
fieldRules:
viewer: [title] # More restrictive
editor: [title, description, author]
fieldEditRules:
viewer: [title]
editor: [title, description]
Terminal window
vski plan # ~ collection default/my_collection: options update
vski apply
ScenarioBehavior
No rules definedAll fields visible (default open)
User has field accessGlobal viewRule/listRule bypassed, filtered fields returned
User has no field accessAccess denied (empty result)
Wildcard in field listAll fields visible
User in multiple groupsUnion of all group fields visible
Admin userAll fields visible (bypasses all rules)
ScenarioBehavior
No rules definedAll fields editable (default open)
User has field accessGlobal createRule/updateRule bypassed, only allowed fields updated
User has no field accessAccess denied (create/update blocked)
Wildcard in field listAll fields editable
User in multiple groupsUnion of all group fields editable
Required field not in listWarning logged, operation proceeds (field may have default)
System fieldsAlways read-only, cannot be edited
Admin userAll fields editable (bypasses all rules)

Field rules are an additional layer of security:

  • Use field rules in addition to global rules
  • Don’t rely solely on field rules for security

When configuring field rules:

  • Start with empty field list (deny all)
  • Add only necessary fields (allow by exception)
  • More secure than starting with ["*"] and removing

If a required field is not in edit rules:

  • The field may get a default value from DB schema
  • Consider using triggers to set appropriate defaults
  • Or add the field to the user’s allowed list

System fields (id, created, updated):

  • Are always read-only for edit rules
  • Cannot be removed from view rules
  • This prevents record tampering

Before deploying:

  1. Test with users in each group
  2. Verify field visibility (read operations)
  3. Verify field editing (create/update operations)
  4. Test global rule bypass scenarios
  5. Verify admin user behavior (should bypass all)

Server logs warnings when:

  • Required fields are not in edit rules
  • Users attempt to edit restricted fields (silently ignored)
  • Monitor these logs to catch configuration issues
options:
fieldRules:
"*": [title, description] # Public can read
fieldEditRules:
editor: [title, description] # Only editors can write
options:
fieldRules:
viewer: [title] # Basic info
member: [title, description] # More info
admin: ["*"] # Full access
options:
fieldRules:
"*": ["*"] # Everyone can read everything
fieldEditRules:
creator: [title, content] # Can create initial content
editor: [content] # Can edit content only
publisher: [published] # Can only change status

Problem: User gets access denied error when listing or viewing records.

Solution: Check if user’s group is in fieldRules:

# User groups
user_groups = ["viewer"]
# Check field rules
collection = await client.settings.collections.get("my_collection")
print("Field rules:", collection["fieldRules"])
# Does user's group exist in fieldRules?
has_access = any(group in collection["fieldRules"] for group in user_groups)
print("Has access:", has_access)

Problem: User updates a record but some fields don’t change.

Solution: Check fieldEditRules - those fields may not be in allowed list:

# User in "viewer" group
await client.collection("my_collection").update(
"record-id",
{
"title": "Updated", # Saved
"status": "active", # NOT saved - not in viewer's edit rules
},
)

Problem: After creating a record, required fields are null.

Solution: Field may have default value. Check:

  1. Field default in schema
  2. Trigger that sets the value
  3. Or add field to user’s fieldEditRules

Problem: User with "*" in field list can’t see all fields.

Solution: Ensure wildcard is in the list, not a bare string:

# Wrong
options:
fieldRules:
viewer: "*"
# Correct
options:
fieldRules:
viewer: ["*"]

Problem: Global rules (createRule, updateRule) still apply even with field rules defined.

Solution: Ensure user has ANY field-level access. Field rules override global rules only when user has field-level permission:

# User in "any_group" has field access: global createRule bypassed
options:
fieldRules:
any_group: [title]
# User in "no_access" has NO field access: global createRule applies
options:
fieldRules:
admin_only: ["*"]