Skip to main content

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.)
  • required must be an array and each required key must exist in properties

“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"
}
Full replacement

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 lets a user attribute store a foreign key to a DataTable record.

Key points:

  • x-reference value 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)

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"
}
About 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-reference attributes 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)

  1. Define schema with a referenced department:
{
"type": "object",
"title": "UserAttributes",
"properties": {
"department_id": {
"type": ["integer", "null"],
"x-reference": "people_departments"
}
},
"required": []
}
  1. Update a user:
{
"attributes": {
"department_id": 5
}
}
  1. 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_id can 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-reference fields, the backend ensures a GIN index exists on auth_user.attributes for 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.