| name | django-spicedb |
| description | Implement django-spicedb in Django application code. Use for translating authorization rules into RebacModel/RebacMeta definitions, choosing FK/M2M/through bindings, wiring runtime checks and queryset filtering, and handling schema publish and tuple sync safely. Use when this capability is needed. |
| metadata | {"author":"flak153"} |
Django Spicedb
Overview
Use this skill when the task is application integration of django-spicedb.
The target is Django app code: models, service layer, and runtime permission calls.
Do not use this skill for modifying internals under django_spicedb/; use $django-spicedb-contributor for that scope.
How The Library Works
Authorization pipeline
- Models inheriting
RebacModel are registered by RebacModelBase.
- Optional external subject/resource models are registered with
register_type(...).
RebacMeta definitions are compiled into a type graph (get_type_graph()).
- The type graph is compiled to SpiceDB schema and published with
publish_schema(...).
- FK/M2M/through changes trigger tuple sync handlers.
- Runtime checks are evaluated through
can(...), .has_perm(...), and .accessible_by(...).
Binding semantics
- FK binding: one row points to one related subject/resource.
- M2M binding: many direct links with no role dimension.
- Through binding: relation value is derived from a role field in a join table.
Use through bindings whenever role transitions must change authorization (member vs manager).
Implementation Workflow
1) Gather inputs before coding
Collect these from the task/request:
- subject types (
User, ServiceAccount, Group)
- protected resource types (
Document, Folder, Verification)
- direct relations (
owner, parent, member, manager)
- inheritance needs (
parent->view, workspace->edit)
- tenancy constraints (single tenant vs tenant-scoped checks)
- write paths used by the app (
save, M2M add/remove, through writes, bulk_create, QuerySet.update)
2) Convert business rules into relation/permission graph
Translate plain-English policy to type-level rules first.
Example translation:
- "Owners can edit documents." -> relation
owner, permission edit = owner
- "Workspace managers can edit workspace documents." -> document relation to workspace,
edit includes workspace->edit
- "Folder access cascades to children." -> relation
parent, permission includes parent->view
Keep permission expressions small and composable.
3) Implement models and RebacMeta
For each protected model:
- inherit from
RebacModel
- add
objects = RebacManager()
- define
RebacMeta.type_name, relations, and permissions
- for role-based memberships, define
RebacMeta.through and set through model manager to RebacThroughManager()
4) Publish schema after relation/permission changes
Use this command:
python manage.py shell -c "import django_spicedb.conf as conf; from django_spicedb.adapters import factory; from django_spicedb.schema import publish_schema; print(publish_schema(factory.get_adapter(), graph=conf.get_type_graph()))"
5) Wire runtime checks in service-layer boundaries
Use service functions as policy boundaries rather than spreading checks ad hoc in every view.
- read endpoints: check
view
- mutation endpoints: check
edit or manage
- list endpoints: use
.accessible_by(...) for server-side filtering
6) Keep write paths sync-safe
Preferred write paths:
instance.save()
- M2M manager methods (
add, remove, clear)
- through-row create/save/delete with
RebacThroughManager
- manager
bulk_create where sync helpers are supported
Use caution with QuerySet.update on authorization-relevant fields; use safer writes when possible.
7) Rollout discipline
- publish schema before switching runtime checks
- cut runtime guards over to
can/has_perm/accessible_by
- keep operational reconcile workflow documented for tuple drift recovery
Worked Example 1: Tenant Workspace + Folder + Document
This pattern covers:
- tenant-scoped workspace membership via through roles
- folder inheritance
- document inheritance from workspace and folder
- service-layer runtime checks and queryset filtering
models.py
from django.contrib.auth import get_user_model
from django.db import models
from django_spicedb.core import register_type
from django_spicedb.integrations.orm import RebacManager, RebacThroughManager
from django_spicedb.models import RebacModel
User = get_user_model()
register_type(User, type_name="user")
class Tenant(models.Model):
slug = models.SlugField(unique=True)
name = models.CharField(max_length=255)
class Workspace(RebacModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name="workspaces")
slug = models.SlugField(max_length=128)
title = models.CharField(max_length=255)
objects = RebacManager()
class Meta:
constraints = [
models.UniqueConstraint(fields=("tenant", "slug"), name="uniq_workspace_per_tenant")
]
class RebacMeta:
type_name = "workspace"
relations = {
"member": {"subject": "user"},
"manager": {"subject": "user"},
}
permissions = {
"view": "member + manager",
"edit": "manager",
}
through = {
"model": "myapp.models.WorkspaceMembership",
: ,
: ,
: ,
: {
: ,
: ,
},
}
(models.Model):
ROLE_MEMBER =
ROLE_MANAGER =
ROLE_CHOICES = [
(ROLE_MEMBER, ),
(ROLE_MANAGER, ),
]
workspace = models.ForeignKey(
Workspace,
on_delete=models.CASCADE,
related_name=,
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name=,
)
role = models.CharField(max_length=, choices=ROLE_CHOICES, default=ROLE_MEMBER)
objects = RebacThroughManager()
:
constraints = [
models.UniqueConstraint(fields=(, ), name=)
]
():
workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name=)
parent = models.ForeignKey(
,
null=,
blank=,
on_delete=models.CASCADE,
related_name=,
)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name=)
name = models.CharField(max_length=)
objects = RebacManager()
:
type_name =
relations = {
: ,
: ,
: ,
}
permissions = {
: ,
: ,
}
():
workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name=)
folder = models.ForeignKey(
Folder,
null=,
blank=,
on_delete=models.CASCADE,
related_name=,
)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name=)
title = models.CharField(max_length=)
body = models.TextField(blank=)
objects = RebacManager()
:
type_name =
relations = {
: ,
: ,
: ,
}
permissions = {
: ,
: ,
}
services.py
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django_spicedb.runtime import can
from django_spicedb.tenant import tenant_context
from .models import Document, Workspace, WorkspaceMembership
def workspace_context(workspace: Workspace) -> dict:
return {
"tenant_id": str(workspace.tenant_id),
"workspace_id": str(workspace.pk),
}
def list_viewable_documents(actor, workspace: Workspace):
ctx = workspace_context(workspace)
return (
Document.objects.filter(workspace=workspace)
.accessible_by(actor, "view", context=ctx)
.select_related("owner", "folder", "workspace")
)
def read_document(actor, workspace: Workspace, document_id: int) -> Document:
document = Document.objects.select_related("workspace", "folder").get(
pk=document_id,
workspace=workspace,
)
with tenant_context(str(workspace.tenant_id)):
allowed = can(actor, "view", document, context=workspace_context(workspace))
if not allowed:
raise PermissionDenied("Missing document:view")
return document
() -> Document:
document = Document.objects.select_for_update().get(pk=document_id, workspace=workspace)
document.has_perm(actor, , context=workspace_context(workspace)):
PermissionDenied()
document.title = title
document.save(update_fields=[])
document
() -> :
workspace.has_perm(actor, , context=workspace_context(workspace)):
PermissionDenied()
rows = [
WorkspaceMembership(
workspace=workspace,
user_id=user_id,
role=WorkspaceMembership.ROLE_MEMBER,
)
user_id user_ids
]
WorkspaceMembership.objects.bulk_create(rows, ignore_conflicts=)
Worked Example 2: Group Membership Roles + Verification Workflow
This pattern covers:
- group membership via through-role relation mapping
- verification resources inheriting group permissions
- role transitions affecting
manage
- service-layer checks for submit/approve/reject flows
models.py
from django.contrib.auth import get_user_model
from django.db import models
from django_spicedb.core import register_type
from django_spicedb.integrations.orm import RebacManager, RebacThroughManager
from django_spicedb.models import RebacModel
User = get_user_model()
register_type(User, type_name="user")
class Group(RebacModel):
slug = models.SlugField(unique=True)
title = models.CharField(max_length=255)
objects = RebacManager()
class RebacMeta:
type_name = "group"
relations = {
"member": {"subject": "user"},
"manager": {"subject": "user"},
}
permissions = {
"view": "member + manager",
"manage": "manager",
}
through = {
"model": "myapp.models.GroupMembership",
"object_fk": "group",
"subject_fk": "user",
"role_field": "role",
"roles": {
"member": "member",
"manager": "manager",
},
}
class GroupMembership(models.Model):
ROLE_MEMBER =
ROLE_MANAGER =
ROLE_CHOICES = [
(ROLE_MEMBER, ),
(ROLE_MANAGER, ),
]
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name=)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name=)
role = models.CharField(max_length=, choices=ROLE_CHOICES, default=ROLE_MEMBER)
objects = RebacThroughManager()
:
constraints = [
models.UniqueConstraint(fields=(, ), name=)
]
():
STATUS_DRAFT =
STATUS_PENDING =
STATUS_APPROVED =
STATUS_REJECTED =
STATUS_CHOICES = [
(STATUS_DRAFT, ),
(STATUS_PENDING, ),
(STATUS_APPROVED, ),
(STATUS_REJECTED, ),
]
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name=)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name=)
title = models.CharField(max_length=)
payload = models.JSONField(default=, blank=)
status = models.CharField(max_length=, choices=STATUS_CHOICES, default=STATUS_DRAFT)
objects = RebacManager()
:
type_name =
relations = {
: ,
: ,
}
permissions = {
: ,
: ,
}
services.py
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from .models import Group, GroupMembership, Verification
def list_visible_verifications(actor, group: Group):
return (
Verification.objects.filter(group=group)
.accessible_by(actor, "view")
.select_related("group", "owner")
)
@transaction.atomic
def submit_verification(actor, group: Group, title: str, payload: dict) -> Verification:
if not group.has_perm(actor, "view"):
raise PermissionDenied("Missing group:view")
return Verification.objects.create(
group=group,
owner=actor,
title=title,
payload=payload,
status=Verification.STATUS_PENDING,
)
def require_manage_access(actor, verification_id: int) -> Verification:
verification = Verification.objects.select_related("group", "owner").get(pk=verification_id)
if not verification.has_perm(actor, "manage"):
raise PermissionDenied("Missing verification:manage")
return verification
@transaction.atomic
def approve_verification(actor, verification_id: ) -> Verification:
verification = require_manage_access(actor, verification_id)
verification.status != Verification.STATUS_PENDING:
ValidationError()
verification.status = Verification.STATUS_APPROVED
verification.save(update_fields=[])
verification
() -> Verification:
verification = require_manage_access(actor, verification_id)
verification.status != Verification.STATUS_PENDING:
ValidationError()
verification.status = Verification.STATUS_REJECTED
verification.save(update_fields=[])
verification
() -> GroupMembership:
membership = GroupMembership.objects.select_related().get(pk=membership_id)
membership.group.has_perm(actor, ):
PermissionDenied()
membership.role = new_role
membership.save(update_fields=[])
membership
() -> :
group.has_perm(actor, ):
PermissionDenied()
rows = [
GroupMembership(group=group, user_id=user_id, role=GroupMembership.ROLE_MEMBER)
user_id user_ids
]
GroupMembership.objects.bulk_create(rows, ignore_conflicts=)
References
references/how-it-works.md for internals from an integrator perspective
references/app-integration.md for additional implementation patterns
references/debugging-playbook.md for incident triage
references/test-playbook.md for optional test strategy
references/claude-codex-prompt-pack.md for reusable prompts
Converted and distributed by TomeVault — claim your Tome and manage your conversions.