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
- A REST API at
http://localhost:8000with 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
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+Create the project
backend/app/main.py is already complete — one line that creates the whole app:Configure the database
- SQLite (default)
- PostgreSQL
.env.example file already points to a local SQLite file:app.db automatically on the first startup.Scaffold the three modules
task-manager/ to create the module skeletons:backend/app/modules/:Write the Team module model
backend/app/modules/team/models.py with: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.Write the Projects module model
backend/app/modules/projects/models.py with:Write the Tasks module model
backend/app/modules/tasks/models.py with: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.Generate API and frontend schemas
task-manager/backend/, run:veloiq generate as a shorthand. This writes two files per module:backend/app/modules/{module}/api.py— standard CRUD endpointsfrontend/src/pages/{module}/{module}Schema.gen.ts— TypeScript field definitions
Start the backend
veloiq db upgrade to apply schema changes via Alembic without restarting the app.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.Start the frontend and log in
http://localhost:5173. You will be redirected to /login.Every VeloIQ application ships with authentication enabled. The first startup seeds a default admin user:admin / admin. You will see the sidebar with Tasks, Projects, Team, and an Access Control group with Users, Roles, and Tenants.What you wrote vs. what the framework generated
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 acustom_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.
Create the custom endpoint file
backend/app/modules/tasks/custom_api.py:Verify the new endpoint
http://localhost:8000/docs — the POST /{task_id}/complete endpoint now appears under the task section, ready to call from any API client.Section 3 — Global search
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 fromGET /config/search and the frontend reads it on startup, querying those fields whenever a user types in the search bar.
Register models and fields
task-manager/backend/, run:config/search.json:_<name>, e.g. task_title matches title) are searched.Review or update the configuration
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 inmain.py, restrict individual models with @model_access, and restrict individual fields with veloiq_field.
Layer 1 — Define global roles
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: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:Layer 2 — Add model-level exceptions with @model_access
backend/app/modules/tasks/models.py and add the decorator:@model_access are unaffected.Layer 3 — Add field-level exceptions with veloiq_field
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.
Regenerate the schema
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.
Apply owner-based access
backend/app/modules/tasks/models.py and add the decorator:assignee_id matches their user ID.Apply tenant isolation (optional)
tenant_field instead:Use relationship traversal with rebac_subquery
rebac_subquery:rebac_subquery builds the subquery for you and handles circular-dependency detection automatically.@rebac applies to all roles including Admin. To exempt Admins, return True from your filter for admin users.- 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
@rebacfrom a model means no row filtering — RBAC layers 1–3 still apply.
Section 6 — Tree views and Miller columns
BecauseTask 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.
Create a task hierarchy
- Create a top-level task: “Launch website”
- Create sub-tasks: “Write copy”, “Design mockups”, “Set up hosting” — each with Parent Task Id pointing to “Launch website”
- Create a sub-sub-task under “Write copy”: “Draft landing page headline”
Navigate the tree
- 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.