Row-Level Security in ormDB
Implement policy-based access control at the database level with ormDB's row-level security. Define who can read, write, and delete specific rows.
Database-Level Access Control
ormDB is a relational database engine written in Rust that replaces PostgreSQL, MySQL, and SQLite. Row-level security (RLS) is built into the engine, allowing you to define access control policies that filter and restrict data at the database level rather than in application code.
RLS policies ensure that no matter how data is accessed, whether through graph fetches, direct entity queries, or change streams, users only see and modify the rows they are authorized to access. This eliminates the class of bugs where application-level authorization checks are missed or bypassed.
Defining Policies
Policies are declared in your ormDB schema alongside entity definitions:
entity Post {
id: uuid @primary
title: text
body: text
status: text
author_id: uuid @relation(User)
tenant_id: uuid
policy read_own {
action: select
condition: author_id == @session.user_id
}
policy read_published {
action: select
condition: status == "published"
}
policy write_own {
action: [insert, update, delete]
condition: author_id == @session.user_id
}
}
In this example, users can read their own posts regardless of status, read any published post, and only modify posts they authored.
Session Context
Policies reference session variables set when the connection is established. Your application sets these variables after authenticating the user:
const db = new OrmDB('ormdb://localhost:5555/mydb')
const session = db.session({
user_id: authenticatedUser.id,
tenant_id: authenticatedUser.tenantId,
role: authenticatedUser.role,
})
// All queries through this session are filtered by RLS policies
const posts = await session.graphFetch('Post', { fields: ['*'] })
// Returns only posts the user is authorized to see
Multi-Tenant Isolation
RLS is the foundation for multi-tenant applications. A tenant isolation policy ensures that all queries, including graph fetches across relations, are scoped to the current tenant:
entity Organization {
id: uuid @primary
name: text
policy tenant_isolation {
action: [select, insert, update, delete]
condition: id == @session.tenant_id
}
}
entity Project {
id: uuid @primary
name: text
org_id: uuid @relation(Organization)
policy tenant_isolation {
action: [select, insert, update, delete]
condition: org_id == @session.tenant_id
}
}
When a user fetches projects with their tasks and comments via a graph fetch, every entity in the traversal is filtered by tenant. See how this powers multi-tenant platforms in practice. There is no possibility of data leakage across tenants, because the filtering happens in the database engine, not in application code.
Policies with Relation Traversal
Policies can reference related entities for complex access rules:
entity Comment {
id: uuid @primary
body: text
post_id: uuid @relation(Post)
author_id: uuid @relation(User)
policy read_if_post_visible {
action: select
condition: post.status == "published" OR author_id == @session.user_id
}
policy delete_if_post_author {
action: delete
condition: post.author_id == @session.user_id OR author_id == @session.user_id
}
}
The post.status and post.author_id references traverse the relation to the parent Post entity. Both the comment author and the post author can delete a comment. This traversal is resolved at the engine level during query execution.
Role-Based Policies
Combine session roles with conditions for role-based access:
entity User {
id: uuid @primary
name: text
email: text
policy admin_full_access {
action: [select, insert, update, delete]
condition: @session.role == "admin"
}
policy user_read_self {
action: select
condition: id == @session.user_id
}
}
Multiple policies on the same entity combine with OR logic for the same action. If any policy grants access, the operation is allowed. This enables layered policies where broad rules (admin access) coexist with narrow rules (self-access).
RLS in ormDB is not an afterthought. It is enforced by the query engine on every operation, including graph fetches of arbitrary depth, ensuring consistent access control across your entire data model. For more on designing schemas that leverage RLS effectively, see schema design patterns.
Frequently Asked Questions
How does ormDB's RLS differ from PostgreSQL's RLS?
ormDB's RLS policies are defined in the schema alongside entity definitions and use the same entity/relation language as queries. PostgreSQL's RLS uses SQL-based policy expressions. ormDB policies also apply to graph fetches, filtering nested relations according to the requesting user's permissions.
Do RLS policies affect graph fetches?
Yes. When a graph fetch traverses a relation, RLS policies on the target entity filter the results. If a user cannot see certain posts, those posts are excluded from the graph fetch result automatically. The parent entity is still returned.
Can I have different policies for read and write operations?
Yes. ormDB supports separate policies for select (read), insert (create), update, and delete operations on each entity. You can allow a user to read all records but only update their own.
How does ormDB know who the current user is?
The session context carries the authenticated user's identity and any custom claims. Your application sets the session context when opening a connection or starting a transaction. Policies reference session variables to make access decisions.
What happens if no policy matches?
By default, if RLS is enabled on an entity and no policy grants access, the operation is denied. This is a deny-by-default model that ensures new entities are secure by default when RLS is active.
Can policies reference related entities?
Yes. Policies can traverse relations. For example, a policy on Comment can check whether the requesting user is the author of the parent Post. Relation traversal in policies uses the same syntax as graph fetches.