Your Django models,
served as a secure API.
One mixin and one method on a model give you the full REST API. Every request is held to the caller's tenant, their own rows, and the fields their role may read.
1from turbodrf.mixins import TurboDRFMixin23class Project(models.Model, TurboDRFMixin):4 title = models.CharField(max_length=200)5 status = models.CharField(max_length=20, default="active")6 workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE)7 client = models.ForeignKey(Client, on_delete=models.CASCADE)8 owner = models.ForeignKey(Member, null=True, on_delete=models.SET_NULL)9 budget = models.DecimalField(max_digits=12, decimal_places=2)101 searchable_fields = ["title"]2 sensitive_fields = ["budget"]1314 @classmethod15 def turbodrf(cls):16 return {3 "fields": [18 "id",19 "title",20 "status",21 "client",22 "client__name",23 "owner__username",24 "budget",25 ],4 "tenant_field": "workspace",5 "owner_field": "owner",28 "bypass_owner_roles": ["admin", "manager"],29 }
- VWebsite relaunchbudget2
- MMobile appnot returned5
- AData warehousenot returned5
- AOld prototypenot returned5
- BPayments APInot returned
- 4Acme's projects only.
- 5viewer keeps the owner rule: just the project he owns.
- 2budget left out: viewer has no budget.read.
Permissions and tenancy
What each caller can do.
Seven requests from five callers: 35 real responses from the same model. Pick any cell to see the exchange and the rule behind it.
Swipe the table to see every caller.
| Request | Aaliceadmin · Acme | Mmayamanager · Acme | Vvictorviewer · Acme | Bbobadmin · Globex | ?anonymousnot signed in |
|---|---|---|---|---|---|
List projects GET /api/projects/ | |||||
Read “Website relaunch” GET /api/projects/1/ · owner victor | |||||
Read “Mobile app” GET /api/projects/2/ · owner maya | |||||
Change its status PATCH /api/projects/1/ {status} | |||||
Change its budget PATCH /api/projects/1/ {budget} | |||||
Use another workspace's client POST /api/projects/ {client} | |||||
Delete “Old prototype” DELETE /api/projects/5/ |
5“Mobile app” belongs to maya, and viewer keeps the owner rule.
{"detail": "No Project matches the given query."}
On every request
Which rows, then which fields.
- Authenticated
Anonymous callers get nothing unless the model is public_access.
- Role allows it
demo.project.read, from TURBODRF_ROLES or the database.
- Rows in scope
The caller's tenant, then owner and predicate rules.
- Fields they can read
Sensitive fields need an explicit .read grant.
- Compiled query
.values() with joins: no model instances to build.
- JSON
Rendered by msgspec or orjson when installed.
Fails closed
Mistakes stop the app starting.
The router checks every model when it starts and refuses to serve an API with any of these:
- A model with no tenancy decision
- A permission string with a typo in the app, model or field
- A Tenant() inside Either(...)
- A searchable field that reaches across a scoped relation
- A nested field deeper than TURBODRF_MAX_NESTING_DEPTH
django.core.exceptions.ImproperlyConfigured: Invoice.turbodrf() declares no tenancy and no tenant FK could be auto-detected to demo.Workspace. Add one of: 'tenant_field': '<path>', 'visibility': [...], or 'tenancy': 'shared' (for reference data). An 'owner_field' alone scopes rows within a tenant, not across tenants. Set TURBODRF_REQUIRE_TENANCY=False to disable this check.
Also generated
Everything else a REST API needs.
The whole REST surface
List, detail, create, update and delete routes, plus OpenAPI, from the model. No view or URL files.
Search, filter, order
Lookups, ordering and ?fields= on every field the caller can read, nothing else.
Predicates when you need them
Owner, Members, Group, Either and Conditional for row rules beyond owner and tenant.
Lean list reads
Lists read values straight from the database, with joins for nested fields, in a fixed number of queries.
OpenAPI per role
Swagger and ReDoc show each caller only the fields their role can read and write.
Roles from your identity provider
Map django-allauth groups or Keycloak / OIDC claims to roles. Sentry hooks included.
demo.Client [compiled] OK
Fields: 1 DB, 0 FK, 0 M2M, 0 property
Public access: False
Tenant model: demo.Workspace
Tenant field: workspace
demo.Project [compiled] OK
Fields: 5 DB, 2 FK, 0 M2M, 0 property
Public access: False
Tenant model: demo.Workspace
Tenant field: workspace
Within-tenant predicates: Owner('owner', bypass=['admin', 'manager']) For you and your coding agent
Ships with an AGENTS.md.
Instructions written for AI coding agents: how to add a model, which tenancy key to pick, and what never to do. turbodrf_check reports each model's tenancy so you can see what it'll enforce.
Watch it run
Fifteen seconds each.
Start with one model.
Install it, add the mixin and the router, give your roles their permissions.