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.

This tutorial walks you through building a working task-management application from scratch: a REST API, a SQLAdmin back-office, and a React CRUD frontend — with almost no code to write. You will create three modules (Team, Projects, and Tasks), add business logic through custom endpoints, lock down access with RBAC and ReBAC, and explore the tree view and built-in UI features that VeloIQ generates automatically. The finished app lives in samples/task-manager/ if you want to compare at any point.

What you’ll build

Three modules, four models:
  • Team — team members with a name, email, and role
  • Projects — projects with a status and an owner (a team member)
  • Tasks — tasks with priority, due date, project, and assignee; plus a self-referential parent/sub-task relationship that renders as an interactive Miller columns tree view
By the end of Section 1 you will have:
  • A REST API at http://localhost:8000 with auto-generated CRUD for all three entities
  • Interactive API docs at http://localhost:8000/docs
  • A SQLAdmin back-office at http://localhost:8000/admin/
  • A React CRUD frontend at http://localhost:5173

Overview

SectionGoalTimeRequired?
Section 1 — Core appHave a working full-stack app in your browser~15 minYes
Section 2 — Custom endpointsAdd business logic beyond standard CRUD~5 minOptional
Section 3 — Global searchWire up the header search bar~5 minOptional
Section 4 — RBACRestrict what roles can do globally, per-model, and per-field~10 minOptional
Section 5 — ReBACFilter which rows each user can see~10 minOptional
Section 6 — Tree viewsNavigate task hierarchies with Miller columns~5 minOptional
Section 7 — Built-in UI featuresExplore the analysis charts, column config, dark mode~5 minOptional
Complete Section 1 first. All optional sections depend only on Section 1, not on each other, so you can read their goal and skip or do them in any order.

Section 1 — Core app

This section gets a working full-stack CRUD app with authentication running in your browser in about 15 minutes. Prerequisites: Python 3.10+, Node.js 18+
1

Create the project

Run the following commands to clone the repository, set up a virtual environment, install the framework, and scaffold the project:
cd ~/projects          # or wherever you keep your code
git clone https://github.com/cesarlugos1s/VeloIQ.git
cd VeloIQ
python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e backend/
veloiq new task-manager
cd task-manager
You get this layout:
task-manager/
├── backend/
│   ├── app/
│   │   ├── main.py          # one line
│   │   └── modules/         # your modules go here
│   ├── .env.example
│   ├── requirements.txt
│   └── api_schema_gen.py
└── frontend/
    ├── src/
    │   ├── App.tsx
    │   └── allModels.gen.ts
    └── package.json
backend/app/main.py is already complete — one line that creates the whole app:
from veloiq_framework import create_veloiq_app
app = create_veloiq_app()
2

Configure the database

Copy the example environment file:
cd backend
cp .env.example .env
Choose your database:
No changes needed. The .env.example file already points to a local SQLite file:
DATABASE_URL=sqlite:///./app.db
SQLite creates app.db automatically on the first startup.
3

Scaffold the three modules

Run these commands from task-manager/ to create the module skeletons:
cd ..                  # back to task-manager/
veloiq add-module team
veloiq add-module projects
veloiq add-module tasks
Each command creates the module skeleton under backend/app/modules/:
backend/app/modules/
├── team/
│   ├── __init__.py
│   └── models.py          ← fill this in
├── projects/
│   ├── __init__.py
│   └── models.py          ← fill this in
└── tasks/
    ├── __init__.py
    └── models.py          ← fill this in
4

Write the Team module model

Once the project skeleton exists, you can hand Steps 4–6 to an AI coding tool instead of typing models by hand. Every project created with veloiq new ships with context files that tell the tool the framework conventions:
ToolContext file loaded automatically
Claude CodeCLAUDE.md
Cursor.cursor/rules/veloiq.mdc + models.mdc
Windsurf.windsurfrules
GitHub Copilot.github/copilot-instructions.md
OpenAI Codex CLIAGENTS.md
Continue.dev.continue/config.json
Open the project root in your tool and paste this prompt:
Build the task-manager app using the VeloIQ framework. Create three modules: teamTeamMember model: name: str, email: str, role: str = "member". Relationships: owned_projects → Project, assigned_tasks → Task. projectsProject model: name: str, description: Optional[str], status: str = "active", FK owner_id → team_member.id. Relationships: owner → TeamMember, tasks → Task. tasksTask model: title: str, description: Optional[str], status: str = "todo", priority: str = "medium", due_date: Optional[datetime.date], planned_work_hours: Optional[float], actual_work_hours: Optional[float], FK project_id → project.id, FK assignee_id → team_member.id, self-referential FK parent_task_id → task.id with subtasks and parent_task relationships. After writing the models run veloiq generate then veloiq db upgrade.
When the AI is done, skip ahead to Step 7.
Replace backend/app/modules/team/models.py with:
from typing import List
from veloiq_framework import TimestampedModel, jm_relationship


class TeamMember(TimestampedModel, table=True):
    __tablename__ = "team_member"

    name: str
    email: str
    role: str = "member"

    owned_projects: List["Project"] = jm_relationship(back_populates="owner")
    assigned_tasks: List["Task"] = jm_relationship(back_populates="assignee")
TimestampedModel adds created_at and updated_at automatically, always placed last in every list, form, and detail view. Use FrameworkModel instead if you do not need audit timestamps.
5

Write the Projects module model

Replace backend/app/modules/projects/models.py with:
from typing import List, Optional
from sqlmodel import Field
from veloiq_framework import TimestampedModel, jm_relationship


class Project(TimestampedModel, table=True):
    __tablename__ = "project"

    name: str
    description: Optional[str] = None
    status: str = "active"

    owner_id: Optional[int] = Field(default=None, foreign_key="team_member.id")
    owner: Optional["TeamMember"] = jm_relationship(back_populates="owned_projects")
    tasks: List["Task"] = jm_relationship(back_populates="project")
6

Write the Tasks module model

Replace backend/app/modules/tasks/models.py with:
import datetime
from typing import List, Optional
from sqlmodel import Field
from veloiq_framework import TimestampedModel, jm_relationship


class Task(TimestampedModel, table=True):
    __tablename__ = "task"

    title: str
    description: Optional[str] = None
    status: str = "todo"        # todo | in_progress | done
    priority: str = "medium"    # low | medium | high | critical
    due_date: Optional[datetime.date] = None
    planned_work_hours: Optional[float] = None
    actual_work_hours: Optional[float] = None

    project_id: Optional[int] = Field(default=None, foreign_key="project.id")
    project: Optional["Project"] = jm_relationship(back_populates="tasks")

    assignee_id: Optional[int] = Field(default=None, foreign_key="team_member.id")
    assignee: Optional["TeamMember"] = jm_relationship(back_populates="assigned_tasks")

    # Self-referential: a task can have sub-tasks
    parent_task_id: Optional[int] = Field(default=None, foreign_key="task.id")
    subtasks: List["Task"] = jm_relationship(
        back_populates="parent_task",
        sa_relationship_kwargs={"foreign_keys": "[Task.parent_task_id]"},
    )
    parent_task: Optional["Task"] = jm_relationship(
        back_populates="subtasks",
        sa_relationship_kwargs={
            "foreign_keys": "[Task.parent_task_id]",
            "remote_side": "[Task.id]",
        },
    )
The parent_task_id foreign key and the subtasks / parent_task relationships give every task an optional parent. The code generator detects the self-referential relationship and automatically sets showViewType: "tree-details" in the TypeScript schema — telling the React UI to render sub-tasks as a Miller columns tree browser instead of a flat table.
7

Generate API and frontend schemas

From task-manager/backend/, run:
cd backend
python api_schema_gen.py
Alternatively, you can run veloiq generate as a shorthand. This writes two files per module:
  • backend/app/modules/{module}/api.py — standard CRUD endpoints
  • frontend/src/pages/{module}/{module}Schema.gen.ts — TypeScript field definitions
8

Start the backend

Install dependencies and start the server:
pip install -r requirements.txt
veloiq run               # http://localhost:8000
The framework creates all database tables automatically on first start — no migration step needed for a fresh project.
After changing your models, run veloiq db upgrade to apply schema changes via Alembic without restarting the app.
Open http://localhost:8000/docs to see a fully documented REST API for all three entities. Open http://localhost:8000/admin/ for the SQLAdmin back-office.
9

Start the frontend and log in

In a second terminal, install frontend dependencies and start the development server:
cd task-manager/frontend
npm install
npm run dev     # http://localhost:5173
Open http://localhost:5173. You will be redirected to /login.Every VeloIQ application ships with authentication enabled. The first startup seeds a default admin user:
UsernamePasswordRoleAccess
adminadminAdminFull CRUD on all resources
Log in with admin / admin. You will see the sidebar with Tasks, Projects, Team, and an Access Control group with Users, Roles, and Tenants.
Before going to production, replace AUTH_SECRET in .env with a long random string (python -c "import secrets; print(secrets.token_hex(32))"), change the default admin password, and remove VELOIQ_AUTH_DISABLED.

What you wrote vs. what the framework generated

You wroteThe framework generated
3 × models.py3 × api.py (full CRUD REST endpoints)
Nothing in main.pyTypeScript schemas, all routers, SQLAdmin views
A self-referential FK in TaskMiller columns tree view — automatically

Section 2 — Custom endpoints

The auto-generated CRUD endpoints cover list, get, create, update, and delete. For anything more specific — status transitions, batch actions, domain operations — you add a custom_api.py in the module directory and import the generated router. The framework loads custom_api.py automatically; no registration in main.py is required.
1

Create the custom endpoint file

Create backend/app/modules/tasks/custom_api.py:
from fastapi import Depends, HTTPException
from sqlmodel import Session

from veloiq_framework import get_session
from .api import router        # the auto-generated router
from .models import Task


@router.post("/{task_id}/complete")
def complete_task(task_id: int, session: Session = Depends(get_session)):
    task = session.get(Task, task_id)
    if task is None:
        raise HTTPException(404, f"Task {task_id} not found")
    task.status = "done"
    session.add(task)
    session.commit()
    session.refresh(task)
    return task.model_dump()
2

Verify the new endpoint

Restart the backend (or let uvicorn hot-reload). Open http://localhost:8000/docs — the POST /{task_id}/complete endpoint now appears under the task section, ready to call from any API client.

Pages are found automatically from the navigation menu. Data search requires you to tell the framework which models and fields to match against. Once configured, the backend serves the search configuration from GET /config/search and the frontend reads it on startup, querying those fields whenever a user types in the search bar.
1

Register models and fields

From task-manager/backend/, run:
veloiq search add-model TeamMember --fields name,email
veloiq search add-model Project    --fields name,description
veloiq search add-model Task       --fields title,description
This creates config/search.json:
{
  "models": ["TeamMember", "Project", "Task"],
  "fields": ["name", "email", "description", "title"]
}
How field matching works: for each model, only fields whose key exactly matches one of the listed names (or ends with _<name>, e.g. task_title matches title) are searched.
2

Review or update the configuration

Use these commands to manage your search configuration at any time:
veloiq search list               # show current config
veloiq search add-field role     # add another field
veloiq search remove-model Project

Section 4 — Role-based access control (RBAC)

VeloIQ ships with a three-layer RBAC system. All layers are purely restrictive — they can narrow access but never grant more than the role’s global permissions allow. You configure global roles in main.py, restrict individual models with @model_access, and restrict individual fields with veloiq_field.
1

Layer 1 — Define global roles

Roles are defined in backend/app/main.py, seeded to the database on startup, and editable at runtime through Access Control → Roles in the sidebar.Replace the contents of backend/app/main.py with:
# backend/app/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 administrative access",        is_preset=True),
        RoleDef("Manager", WRITE_METHODS, "Create, edit and view — no delete", is_preset=True),
        RoleDef("Viewer",  READ_METHODS,  "Read-only access",                  is_preset=True),
        # Add custom roles as needed:
        RoleDef("Auditor", READ_METHODS,  "External auditor — read only",      is_preset=True),
    ],
))
The Auditor role is upserted to the database on the next startup and immediately appears in Access Control → Roles.To test different roles, create users via Access Control → Users and assign roles:
UsernameRoleCan do
aliceManagerCreate and edit — no delete
carolViewerRead-only
2

Layer 2 — Add model-level exceptions with @model_access

Restrict which actions a role may perform on one specific model, without changing that role’s permissions on every other resource.Open backend/app/modules/tasks/models.py and add the decorator:
from veloiq_framework import TimestampedModel, jm_relationship, model_access

@model_access(Viewer=["list", "show"])
class Task(TimestampedModel, table=True):
    ...
A Viewer can list and view tasks but can never create, edit, or delete one — even if their global permissions are expanded later. Roles not listed in @model_access are unaffected.
3

Layer 3 — Add field-level exceptions with veloiq_field

Control which roles can read or write individual fields:
from veloiq_framework import TimestampedModel, jm_relationship, veloiq_field

class Task(TimestampedModel, table=True):
    __tablename__ = "task"

    title: str
    status: str = "todo"

    # Only managers and admins can set work-hour estimates
    planned_work_hours: Optional[float] = veloiq_field(
        default=None, write_roles=["Admin", "Manager"]
    )
    # Actual hours logged — Admin-only write, everyone can read
    actual_work_hours: Optional[float] = veloiq_field(
        default=None, write_roles=["Admin", "Manager"]
    )
  • write_roles — roles that may set this field. Others’ payloads are silently filtered.
  • read_roles — roles that may see this field. Others receive the record without it.
4

Regenerate the schema

After changing model annotations, regenerate so the frontend also hides or disables restricted inputs:
cd backend && python api_schema_gen.py
The generator emits readRoles / writeRoles into the TypeScript schema automatically.

Section 5 — Row-level access control (ReBAC)

Use @rebac when access depends on the data itself rather than a role — for example, a user should only see tasks assigned to them. ReBAC filters which rows each user can access based on data ownership or relationship traversal.
1

Apply owner-based access

Open backend/app/modules/tasks/models.py and add the decorator:
from veloiq_framework import TimestampedModel, jm_relationship, rebac

@rebac(owner_field="assignee_id")
class Task(TimestampedModel, table=True):
    __tablename__ = "task"

    assignee_id: Optional[int] = Field(default=None, foreign_key="team_member.id")
    ...
With this decorator, every list/get/update/delete endpoint filters rows so a user only sees tasks where assignee_id matches their user ID.
2

Apply tenant isolation (optional)

To isolate rows by tenant, use tenant_field instead:
@rebac(tenant_field="tenant_id")
class Contract(TimestampedModel, table=True):
    tenant_id: int = Field(foreign_key="veloiq_tenant.id")
3

Use relationship traversal with rebac_subquery

When access follows a chain — for example, tasks are accessible when their project is accessible — use rebac_subquery:
from veloiq_framework import rebac, rebac_subquery

@rebac(owner_field="owner_id")
class Project(TimestampedModel, table=True):
    owner_id: Optional[int] = Field(default=None, foreign_key="team_member.id")

@rebac(filter=lambda user, cls, session:
           cls.project_id.in_(rebac_subquery(Project, user, session)))
class Task(TimestampedModel, table=True):
    ...
rebac_subquery builds the subquery for you and handles circular-dependency detection automatically.
No automatic Admin bypass@rebac applies to all roles including Admin. To exempt Admins, return True from your filter for admin users.
Key points:
  • 404 not 403 — accessing an existing but inaccessible row returns 404 to prevent leaking which record IDs exist.
  • Multiple patterns (filter, owner_field, tenant_field) on one decorator are OR-combined: a row is visible if any pattern allows it.
  • Omitting @rebac from a model means no row filtering — RBAC layers 1–3 still apply.

Section 6 — Tree views and Miller columns

Because Task has a self-referential parent_task_id foreign key, the code generator automatically configured the sub-tasks relation as showViewType: "tree-details" in the TypeScript schema. No extra code is needed — the tree view is available as soon as you have hierarchical data.
1

Create a task hierarchy

Create the following tasks in the UI or via the API:
  1. Create a top-level task: “Launch website”
  2. Create sub-tasks: “Write copy”, “Design mockups”, “Set up hosting” — each with Parent Task Id pointing to “Launch website”
  3. Create a sub-sub-task under “Write copy”: “Draft landing page headline”
2

Navigate the tree

Open the Show page for “Launch website”. The Subtasks panel renders as a Miller columns browser: each column shows the children of the selected item. Click any sub-task — a new column slides in to the right. The breadcrumb trail at the top of each column lets you navigate back up the tree without leaving the page.Additional navigation options:
  • Drag the vertical handle between two columns to resize them.
  • Each row has a (open in new tab) button — click it to open that task’s Show page in a full browser tab.
  • The Tasks list page uses the same row-click behaviour in a side-by-side layout: click a row and a detail panel slides in from the right.

Section 7 — Built-in UI features

All of the following features are available out of the box — no code required. They apply to every list and detail view generated by VeloIQ. Analysis charts — open the Show page for a project that has several tasks. The Analyse panel renders distribution charts for the relation’s columns (status, priority, numeric fields). Click the bar-chart icon in the table toolbar to toggle it; your preference is saved. Column configuration — click the settings icon in any list or relation table toolbar to open the column configuration panel. Tick or untick columns to show or hide them, and drag to reorder. Preferences are saved per user and per view. Sorting — click any column header to sort ascending, again to sort descending, a third time to clear the sort. Filtering — hover over a column header to reveal the filter icon. String columns support contains/starts-with; number and date columns support range operators. Multiple filters stack. Dark mode — the toggle in the top-right corner of the header switches between light and dark themes. The preference is stored in the browser. Metadata — the button in a list toolbar opens a modal with technical details about the resource: field names, types, relation targets, and the API path. Relations explorer — the explore button on any Show page opens an interactive graph visualising all the relations connected to that record. Click any node to navigate directly to that record. Bulk actions — tick the checkboxes on one or more rows in any list. A bulk-action toolbar appears at the bottom with bulk edit, bulk delete, and CSV export.

Next steps

Custom endpoints

Learn how to extend generated routers with domain-specific logic and status transitions.

Access control

Deep dive into RBAC and ReBAC concepts, including combining multiple patterns.

Configuration reference

Configure auth, CORS, Alembic migrations, and all other framework settings.

Open-core model

See the full list of free and Pro features available in VeloIQ.