User Attributes
User Attributes are tenant-scoped custom fields stored on each user in auth_user.attributes (a JSONB/JSONField). They let you extend the user profile without DB migrations, and they unlock attribute-based access control (ABAC) patterns when used with Cerbos.
What are User Attributes?
User attributes are:
- Dynamic: Each tenant can define its own attributes.
- Validated: Stored values are validated against a tenant-defined JSON Schema.
- Safe by default: Unknown keys are rejected (effectively
additionalProperties: false).
Common use cases:
- Department/team membership
- Feature flags and entitlements
- Org-specific identifiers (employee number, cost center)
- Data access scopes (region, business unit, customer tier)
Core concepts & rules
JSON Schema format
- Uses JSON Schema Draft 2020-12
- The schema must be an object schema:
"type": "object" - Attribute names must be lowercase snake_case and start with a letter
- Reserved user field names are blocked (e.g.
email,username,attributes,is_active, etc.) requiredmust be an array and each required key must exist inproperties
“Allow null”
Use union types to allow missing/nullable values:
{
"type": ["string", "null"]
}
API: Schema management
User Attributes schema is managed via the tenant settings API.
Get current schema
GET /api/settings/user-attributes/
Response shape:
{
"schema": { },
"has_schema": false,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
Update schema
POST /api/settings/user-attributes/ (requires manage_site)
Request can be either:
- raw schema object, or
- wrapper
{ "schema": { ... } }
Response shape:
{
"schema": { },
"created": true,
"updated_at": "2026-01-01T00:00:00Z",
"message": "Schema created successfully"
}
Schema updates replace the entire schema. Workflow: GET current schema → modify → POST full schema.
API: Writing user attributes
User attributes are written via the user update endpoint.
PUT /api/users/{username}/
Example payload:
{
"attributes": {
"department": "Engineering",
"employee_id": "EMP00123"
}
}
Merge behavior: updates merge into existing attributes (keys you send overwrite; other keys stay).
x-reference: link user attributes to DataTables
x-reference lets a user attribute store a foreign key to a DataTable record.
Key points:
x-referencevalue is the DataTable physical table name (e.g.people_departments)- Referenced table must exist and be materialized
- Backend validates referenced record(s) exist
- Backend enforces strict typing based on the referenced table primary key kind (UUID vs integer)
UUID vs integer references (recommended schema)
If the referenced DataTable primary key is UUID:
Scalar:
{
"type": ["string", "null"],
"format": "uuid",
"x-reference": "people_teams"
}
Array:
{
"type": ["array", "null"],
"items": { "type": "string", "format": "uuid" },
"x-reference": "people_teams"
}
If the referenced DataTable primary key is integer:
Scalar:
{
"type": ["integer", "null"],
"x-reference": "people_departments"
}
Array:
{
"type": ["array", "null"],
"items": { "type": "integer" },
"x-reference": "people_departments"
}
format: "uuid"format: "uuid" is recommended for clarity and tooling, but UUID-ness is enforced by backend reference validation rather than JSON Schema format-checking.
How Import Reference relates
Import Reference creates a DataTable “reference” record in another app that points to the same physical table (no duplication). It:
- Prevents reference chains (reference → reference)
- Keeps schemas synced from source → references
For user attributes, this is useful because it lets multiple apps share the same canonical tables (e.g., People app tables) while your user attributes can reference them consistently via x-reference.
How attributes show up in Cerbos
Cerbos principal attributes include the user’s attributes (plus standard user fields like email, first_name, etc.).
Current behavior:
- Scalar
x-referenceattributes may be auto-resolved into the referenced record object when possible. - Array-typed references remain raw IDs (current behavior).
End-to-end example (schema → user update → policy)
- Define schema with a referenced department:
{
"type": "object",
"title": "UserAttributes",
"properties": {
"department_id": {
"type": ["integer", "null"],
"x-reference": "people_departments"
}
},
"required": []
}
- Update a user:
{
"attributes": {
"department_id": 5
}
}
- Cerbos policy concept (example snippet):
condition:
match:
expr: request.resource.attr.department_id == principal.attr.department_id.id
Notes:
- If auto-resolution happens,
principal.attr.department_idcan become an object like{ "id": 5, "name": "Engineering", ... }. - If resolution fails, you may see the raw scalar ID instead. Prefer writing policies that handle your chosen convention consistently.
Querying/filtering & performance notes
- When your schema contains any
x-referencefields, the backend ensures a GIN index exists onauth_user.attributesfor performance. - List filtering uses JSONB containment queries (e.g.
attributes @> {...}) for indexed lookups.
Troubleshooting
- Type mismatch (UUID vs integer): align the schema type with the referenced table PK kind.
- Referenced table not found / not materialized: make sure the DataTable exists and is materialized.
- Referenced record missing: ensure the referenced ID exists in the DataTable.