Skip to main content
VeloIQ ships with four layers of access control that go from coarse to fine: global role permissions, model-level exceptions, field-level exceptions, and row-level filtering. All four layers are opt-in and can be combined freely on any model.

RBAC — role-based access control

The three RBAC layers work together as a funnel. Each layer can only narrow the access that the layer above it grants — it can never expand it.

Layer 1 — Global role permissions

Define your roles in VeloIQConfig and pass them to create_veloiq_app(). Each RoleDef maps a role name to a set of allowed HTTP methods. VeloIQ seeds these roles to the database on startup and makes them editable at runtime through the admin UI. Use the built-in constants to assign method sets:

Layer 2 — Model-level exceptions

Use the @model_access decorator to override which actions a role may perform on a specific model. Roles not listed in @model_access continue to inherit their global permissions unchanged.
Exceptions are restrictive only. @model_access can reduce what a role may do on a model — it cannot grant actions that the role’s global permissions don’t already include.

Layer 3 — Field-level exceptions

Use veloiq_field() to restrict read and write access per field. The backend CRUD router enforces these at runtime, and veloiq generate emits readRoles and writeRoles into the TypeScript schema so the frontend can hide or disable restricted inputs automatically.
veloiq_field() accepts all the same keyword arguments as pydantic.Field and is fully compatible with TimestampedModel and FrameworkModel.

ReBAC — relationship-based access control

ReBAC applies row-level filtering based on the data itself. Use it when access depends on ownership, tenant membership, or any relationship between a user and a record. Apply the @rebac decorator to a model class; the framework then filters every query for that model before returning results.

@rebac(owner_field=...)

Restrict each user to only the rows they created. Pass the name of the column that holds the owner’s user ID.

@rebac(tenant_field=...)

Isolate rows by tenant. Each user sees only rows whose tenant_id matches a tenant they belong to.

@rebac(filter=...)

For custom access logic, pass a lambda that receives the current user, the model class, and the database session and returns a SQLAlchemy WHERE clause. Use rebac_subquery() to inherit access from a parent model through a relationship.

rebac_subquery()

rebac_subquery(ModelClass, user, session) returns a subquery of primary keys from ModelClass that the current user may access. The target model must itself carry a @rebac decorator. Circular dependencies between models raise a ValueError at runtime. Use it to chain access through a relationship: if a user can access a Folder, they can access the Document rows inside it.

Key behaviors

@rebac applies to all roles, including Admin. To exempt Admins, return True from the filter lambda when the user has the Admin role.
  • Inaccessible rows return 404, not 403. This prevents leaking information about which IDs exist.
  • Multiple patterns are OR-combined. If you pass more than one of filter, owner_field, and tenant_field to the same @rebac call, a row is visible if any single pattern allows it.

After changing models

Run veloiq generate after adding or changing any veloiq_field(), @model_access, or @rebac annotation. This updates the TypeScript schemas so the frontend enforces the same restrictions as the backend.

Models

Define fields and use veloiq_field() for field-level access

Modules

Understand how modules and auto-loading work