Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.veloiq.dev/llms.txt

Use this file to discover all available pages before exploring further.

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:
ConstantAllowed methods
ALL_METHODSGET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
WRITE_METHODSGET, POST, PUT, PATCH, OPTIONS, HEAD
READ_METHODSGET, OPTIONS, HEAD
# main.py
from veloiq_framework import (
    create_veloiq_app, VeloIQConfig,
    RoleDef, ALL_METHODS, WRITE_METHODS, READ_METHODS,
)

app = create_veloiq_app(VeloIQConfig(
    roles=[
        RoleDef("Admin",   ALL_METHODS,   "Full access",               is_preset=True),
        RoleDef("Manager", WRITE_METHODS, "Create/edit, no delete",    is_preset=True),
        RoleDef("Viewer",  READ_METHODS,  "Read-only",                 is_preset=True),
        # Add as many custom roles as needed:
        RoleDef("Auditor", READ_METHODS,  "External auditor",          is_preset=True),
    ],
))

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.
from veloiq_framework import model_access, TimestampedModel

@model_access(Viewer=["list", "show"])   # Viewer is read-only on Invoice
class Invoice(TimestampedModel, table=True):
    __tablename__ = "invoice"
    amount: float
    status: str = "draft"
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.
from veloiq_framework import veloiq_field, TimestampedModel

class Employee(TimestampedModel, table=True):
    __tablename__ = "employee"
    name: str
    department: str
    # Only Admins can see or change salary
    salary: float = veloiq_field(
        default=0.0,
        read_roles=["Admin"],
        write_roles=["Admin"],
    )
    # Viewers can read notes, but only Managers and Admins can write them
    notes: str = veloiq_field(
        default="",
        write_roles=["Admin", "Manager"],
    )
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.
from sqlmodel import Field
from veloiq_framework import rebac, TimestampedModel

@rebac(owner_field="created_by")
class Note(TimestampedModel, table=True):
    __tablename__ = "note"
    created_by: int = Field(foreign_key="veloiq_user.id")
    body: str

@rebac(tenant_field=...)

Isolate rows by tenant. Each user sees only rows whose tenant_id matches a tenant they belong to.
from sqlmodel import Field
from veloiq_framework import rebac, TimestampedModel

@rebac(tenant_field="tenant_id")
class Contract(TimestampedModel, table=True):
    __tablename__ = "contract"
    tenant_id: int = Field(foreign_key="veloiq_tenant.id")
    title: str

@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.
from veloiq_framework import rebac, rebac_subquery, TimestampedModel

@rebac(filter=lambda user, cls, session:
           cls.folder_id.in_(rebac_subquery(Folder, user, session)))
class Document(TimestampedModel, table=True):
    __tablename__ = "document"
    folder_id: int
    name: str

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.
veloiq generate

Models

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

Modules

Understand how modules and auto-loading work