Rls schema — Security Protocol reference
Implements fine-grained record-level access control inspired by PostgreSQL RLS and Salesforce Criteria-Based Sharing Rules.
Row-Level Security (RLS) Protocol
Implements fine-grained record-level access control inspired by PostgreSQL RLS and Salesforce Criteria-Based Sharing Rules.
Overview
Row-Level Security (RLS) allows you to control which rows users can access in database tables based on their identity and positions. Unlike object-level permissions (CRUD), RLS provides record-level filtering.
Use Cases
-
Multi-Tenant Data Isolation
- Users only see records from their organization
using: "organization_id == current_user.organization_id"
-
Ownership-Based Access
- Users only see records they own
using: "owner_id == current_user.id"
-
Organization Member Visibility
- Users see fellow members of their active organization
using: "id in current_user.org_user_ids"(org_user_idsis pre-resolved by the runtime)
-
Territory / Regional Access (§7.3.1 dynamic membership)
- Sales reps only see accounts in their assigned territories
using: "account_id in current_user.territory_account_ids"(the runtime stagesterritory_account_idsinExecutionContext.rlsMembership)
-
Manager / Hierarchy Access (§7.3.1 dynamic membership)
- Managers see records assigned to anyone they manage
using: "assigned_to_id in current_user.team_member_ids"(the runtime pre-resolvesteam_member_ids, no subquery needed)
PostgreSQL RLS Comparison
PostgreSQL RLS Example:
CREATE POLICY tenant_isolation ON accounts
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
CREATE POLICY account_insert ON accounts
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);ObjectStack RLS Equivalent:
{
name: 'tenant_isolation',
object: 'account',
operation: 'select',
using: 'organization_id == current_user.organization_id'
}Salesforce Sharing Rules Comparison
Salesforce uses "Sharing Rules" and a visibility hierarchy for record-level access (our equivalent hierarchy is the business-unit tree, ADR-0090 D3). ObjectStack RLS provides similar functionality with more flexibility.
Salesforce:
- Criteria-Based Sharing: Share records matching criteria with users/groups
- Owner-Based Sharing: Share records based on who owns them
- Manual Sharing: Individual record sharing
ObjectStack RLS:
- A constrained CEL predicate grammar: comparisons and set-membership against literals or
current_user.*values, composable with&&/||; anything that does not lower to a filter fails closed - Subquery-shaped needs are pre-resolved by the runtime (§7.3.1)
- Multiple policies OR-combine on reads (any match allows); for writes see
check
Best Practices
- Always Define SELECT Policy: Control what users can view
- Define INSERT/UPDATE CHECK Policies: Prevent data leakage
- Use Position-Scoped Policies: Apply different rules to different positions
- Test Thoroughly: RLS can have complex interactions
- Monitor Performance: Complex RLS policies can impact query performance
Security Considerations
- Defense in Depth: RLS is one layer; use with object permissions
- Default Deny, among the policies that apply: once a policy applies to the caller and the operation, a row that none of them admits is denied, and a policy that cannot be evaluated fails closed. When no policy applies, the policies restrict nothing (the tenant wall, a separate layer, still applies): object permissions, not RLS policies, stop an un-permissioned user
- Context Variables: Ensure current_user context is always set
See also: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
See also: https://help.salesforce.com/s/articleView?id=sf.security_sharing_rules.htm
Source: packages/spec/src/security/rls.zod.ts
TypeScript Usage
import { RLSEvaluationResultSchema, RLSOperation, RLSUserContextSchema, RowLevelSecurityPolicySchema } from '@objectstack/spec/security';
import type { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security';
// Validate data
const result = RLSEvaluationResultSchema.parse(data);RLSEvaluationResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| policyName | string | ✅ | Policy name |
| granted | boolean | ✅ | Whether access was granted |
| durationMs | number | optional | Evaluation duration in milliseconds |
| error | string | optional | Error message if evaluation failed |
| usingResult | boolean | optional | USING clause evaluation result |
| checkResult | boolean | optional | CHECK clause evaluation result |
RLSOperation
Allowed Values
selectinsertupdatedeleteall
RLSUserContext
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | User ID |
string | optional | User email | |
| tenantId | string | optional | Tenant/Organization ID |
| positions | string[] | optional | Positions held by the user |
| department | string | optional | User department |
| attributes | Record<string, any> | optional | Additional custom user attributes |
RowLevelSecurityPolicy
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Policy unique identifier (snake_case) |
| label | string | optional | Human-readable policy label |
| description | string | optional | Policy description and business justification |
| object | string | ✅ | Target object name |
| operation | Enum<'select' | 'insert' | 'update' | 'delete' | 'all'> | ✅ | Database operation this policy applies to |
| using | string | optional | Row predicate, authored in canonical CEL (ADR-0058 D1): the rows a select policy lets a caller read, and the existing rows an update or delete policy lets a caller change or remove. On an insert or an update, when no applicable policy for that operation declares check, each applicable policy's using also stands in as its check on every row written (see check); on an insert policy that is its only effect. It enforces when the predicate lowers to an ObjectQL filter: a field compared against a literal or a current_user.* context value using ==, !=, <, <=, > or >=; in against a current_user.* array or an inline literal list (e.g. status in ['draft', 'pending']); these combined with && / ||; or the bare allow-all true. Anything that does not lower fails closed — the policy matches zero rows. The legacy SQL-ish spellings are still accepted through a transitional bridge that rewrites = to == and IN to in (deprecated under ADR-0058 D1); SQL AND / OR / NOT IN / IS NULL / LIKE are NOT bridged and fail closed. Needed on a select or delete policy (a check there is refused); optional on an insert, update or all policy that declares check. |
| check | string | optional | Validation condition judged on every row an insert or an update writes (enforced at application level): each row of an insert, an array insert included, as its beforeInsert hooks leave it, and each row an update changes, by id or multi: true, as the prior row merged with the final payload after its beforeUpdate hooks. One failing row refuses the whole write. A by-id update is also judged, before its hooks, on the prior row merged with the change set as sent. The default to using is decided per operation across the applicable policies, not per policy: when any applicable policy for that operation declares check, only the declared checks decide (OR-combined) and a USING-only sibling adds nothing; only when none declares check does each applicable policy's using stand in as its check (OR-combined). Applicable = not enabled: false, object matches or is '*', operation matches or is 'all', and the caller holds one of its positions when it lists any. Refused on a policy whose operation is select or delete, which write no new row to check: limit the rows such a policy admits with using, and declare the check on an insert, update or all policy. |
| positions | string[] | optional | Positions this policy applies to (omit for all) |
| enabled | boolean | optional (default: true) | Whether this policy is active |
| priority | never | optional | [REMOVED] rowLevelSecurity[].priority was removed in @objectstack/spec 17.0.0. It never had an effect. Delete the key — policy outcomes are unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| tags | never | optional | [REMOVED] rowLevelSecurity[].tags was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler never consulted a policy's tags and nothing else acted on them, so a tag scoped, restricted and reported nothing. Delete the key. A tag never limited whom a policy applies to; to do that, list the positions in positions. A policy is identified by its name and its object; say why it exists in description. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
Permission
Permission schemas of the ObjectStack Security Protocol: AdminScope and 5 more — each property with its type, default and a TypeScript example.
Sharing
Sharing schemas of the ObjectStack Security Protocol: CriteriaSharingRule, OWDModel and 4 more — each property with its type, default and a TypeScript example.