User Types
Define the custom fields of users with a JSON schema.
A user type is the schema of a kind of user. It declares the fields that users of that type have, beyond the standard ones, together with their types and validation rules. Every user create and update is validated against the schema of the user's type.
With user types you can, without changing any code:
- add custom fields (a phone number, a birth date, a list of addresses, a loyalty tier…),
- make fields required, unique, or limited to a set of values,
- build a hierarchy of types (Customer and Employee both extending Person),
- let a back office generate forms from the schema.
Inheritance#
User types inherit from each other through baseType. Every chain ends in the built-in base-user type, which declares the standard fields of all users:
| Field | Type | Rules |
|---|---|---|
firstname | string | required |
lastname | string | |
username | string | required, unique per membership |
email_address | email | required, unique per membership |
role | string | required |
permissions | array of string | unique items |
forbidden | array of string | unique items |
user_type | string | required |
source_provider | string | read-only |
connected_accounts | array of objects | read-only |
is_active | boolean | read-only |
membership_id | string | read-only |
sys | object | managed by the server |
base-user is abstract: no user can have it as their type, you always create your own types. It is not stored in the database and is not returned by the list endpoint, but GET /user-types/all and GET /user-types/base-user include it.
base-user (abstract)
└── person (abstract) + phone, birth_date
├── customer + loyalty_tier, addresses
└── employee (sealed) + department, employee_noA type has all the fields of its ancestors plus its own.
| Flag | Meaning |
|---|---|
isAbstract | No user can have this type; it only serves as a base for other types. |
isSealed | No other type can inherit from this type. |
A type can't be both abstract and sealed, and a chain can't loop back on itself.
The user type object#
{
"_id": "66f1c0d2a4b5c6d7e8f90130",
"name": "Customer",
"slug": "customer",
"description": "People who buy from us",
"baseType": "person",
"isAbstract": false,
"isSealed": false,
"allowAdditionalProperties": false,
"properties": {
"loyalty_tier": {
"type": "enum",
"displayName": "Loyalty Tier",
"items": [
{ "displayName": "Bronze", "value": "bronze" },
{ "displayName": "Silver", "value": "silver" },
{ "displayName": "Gold", "value": "gold" }
],
"defaultValue": "bronze"
},
"customer_no": {
"type": "string",
"isRequired": true,
"isUnique": true,
"regexPattern": "^C[0-9]{8}$"
},
"addresses": {
"type": "array",
"maxCount": 5,
"itemSchema": {
"type": "object",
"properties": {
"label": { "type": "string", "isRequired": true },
"city": { "type": "string", "isRequired": true },
"location": { "type": "location" }
}
}
}
},
"membership_id": "66f1c0d2a4b5c6d7e8f90123",
"sys": { "created_at": "2026-01-01T12:00:00Z", "created_by": "admin" }
}| Field | Required | Description |
|---|---|---|
name | yes | Display name. Base User is reserved. |
slug | no | Derived from the name when omitted. base-user is reserved. Users refer to their type by this slug. |
description | no | |
baseType | no | Slug or name of the base type; stored as the slug. base-user when omitted. |
isAbstract, isSealed | no | See Inheritance. false by default. |
allowAdditionalProperties | no | When true, users may have fields that the schema does not declare. false by default: undeclared fields are rejected. |
properties | yes | The fields declared by this type, as an object keyed by field name. Inherited fields are not repeated. |
Fields#
Each entry of properties describes one field. The key is the field name as it appears on users.
Common options#
| Option | Type | Description |
|---|---|---|
type | string | The field type (see below). Required. |
displayName | string | Human-readable name, for forms. |
description | string | Help text, for forms. |
isRequired | boolean | The field must have a value. For strings, whitespace-only values count as empty. |
defaultValue | any | Value used when the field is not given on creation. |
isUnique | boolean | No two users may share a value (see Unique fields). Available on string, integer, float, boolean, enum and the string-based types. |
isVirtual | boolean | Lets a derived type redeclare the field (see Redeclaring inherited fields). |
isHidden, isReadonly | boolean | Hints for user interfaces. A hidden field that is required must have a defaultValue. |
appearance | string | Hint for user interfaces (how to render the field). |
isSearchable, searchWeight | boolean, number | Hints for search interfaces. |
isHidden, isReadonly, appearance, isSearchable and searchWeight are stored and returned for user interfaces such as a back office; the API does not enforce them on your own fields.Field types#
Primitive types
| Type | Value | Options |
|---|---|---|
string | text | minLength, maxLength, regexPattern (the value must match), restrictRegexPattern (the value must not match), formatPattern, caseInsensitive |
integer | whole number | minimum, maximum (inclusive), exclusiveMinimum, exclusiveMaximum, multipleOf |
float | number | minimum, maximum, exclusiveMinimum, exclusiveMaximum |
boolean | true / false | |
enum | one of the items | items (required, unique, { "displayName", "value" }), isMultiple (the value is an array of item values) |
const | a fixed value | value, valueType |
object | nested object | properties (same format as the type's properties), allowAdditionalProperties |
array | list | itemSchema (a field definition for the items), minCount, maxCount, uniqueItems, uniqueBy (field names that must be unique among object items) |
Formatted types
| Type | Value | Options |
|---|---|---|
email | an email address | string options |
uri | an absolute URI | string options |
hostname | a host name | string options |
color | a color code, e.g. #1E90FF | string options |
date | yyyy-MM-dd | minValue, maxValue |
datetime | ISO 8601 date and time, e.g. 2026-01-01T12:00:00Z | minValue, maxValue |
longtext | multi-line text | string options |
richtext | HTML text | minWordCount, maxWordCount |
code | source code | string options |
json | any JSON value | |
tags | array of strings | minCount, maxCount, minLength, maxLength (of each tag) |
location | { "latitude": 41.01, "longitude": 28.97 } | |
image, video | media descriptors | size and dimension rules (maxSize, minWidth, maxWidth…) |
reference | a reference to other users | referenceType (single, multiple or collection), contentType (the user type slug the referenced users must be of, or inherit from; base-user accepts any user) |
Date and time values are stored in UTC. A datetime without an offset is read as UTC.
Examples#
A phone number:
"phone": {
"type": "string",
"displayName": "Phone",
"maxLength": 20,
"regexPattern": "^\\+?[0-9 ()-]{7,20}$"
}A birth date that must be in the past century:
"birth_date": {
"type": "date",
"minValue": "1926-01-01"
}A list of interests chosen from a set:
"interests": {
"type": "enum",
"isMultiple": true,
"items": [
{ "displayName": "Sports", "value": "sports" },
{ "displayName": "Music", "value": "music" },
{ "displayName": "Travel", "value": "travel" }
]
}The manager of an employee, who must be an employee too:
"manager": {
"type": "reference",
"referenceType": "single",
"contentType": "employee"
}Unique fields#
When a field is marked isUnique, ErtisAuth creates a MongoDB unique index for it:
- Uniqueness is checked within the membership, among the users of the type that declares the field unique and of the types derived from it.
- Empty values are not counted: many users may leave a unique field empty.
usernameandemail_addressare unique within the membership.
When you mark a field as unique and some users already share a value, the user type change is rejected with 409 UniqueFieldHasDuplicates, which names the duplicate. Clean up the duplicates first.
A user write with a duplicate value answers 400 ValidationException with a field error (see Users).
Unique fields inside array items are checked by the application, not by an index.
Redeclaring inherited fields#
A type can't declare a field that one of its base types already declares (400 SchemaValidationException, "field is already exist in base type"). The exception is a field declared with isVirtual: true in the derived type: it is accepted as long as its type is the same as the inherited one (otherwise 400 SchemaValidationException, "The field type cannot be overwritten on virtual fields").
Endpoints#
All routes are under /memberships/{membershipId}.
| Method | Route | Description | Permission |
|---|---|---|---|
GET | /user-types/{id} | Get a user type (id or slug) | user-types.read.{id} |
GET | /user-types | List the stored user types | user-types.read |
GET | /user-types/all | All user types, base-user included, without pagination | user-types.read |
GET | /user-types/relations/{id} | The declaring type of each field | user-types.read.{id} |
POST | /user-types/_query | Query user types | user-types.read |
POST | /user-types | Create a user type | user-types.create |
PUT | /user-types/{id} | Update a user type | user-types.update.{id} |
DELETE | /user-types/{id} | Delete a user type | user-types.delete.{id} |
Get a user type#
GET /memberships/{membershipId}/user-types/{id}Returns the type with the fields inherited from its base types.
Field relations#
GET /memberships/{membershipId}/user-types/relations/customerGroups the fields of a type by the type that declares them, up to base-user. Useful to build forms with one section per level:
{
"base-user": [ "firstname", "lastname", "username", "email_address", "role", "…" ],
"person": [ "phone", "birth_date" ],
"customer": [ "loyalty_tier", "customer_no", "addresses" ]
}Create a user type#
curl -X POST https://auth.example.com/memberships/<membership_id>/user-types \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "Employee",
"baseType": "person",
"isSealed": true,
"properties": {
"department": { "type": "string", "isRequired": true },
"employee_no": { "type": "integer", "isUnique": true, "minimum": 1 }
}
}'Response 201 Created: the user type.
| Error | When |
|---|---|
400 UserTypeNameRequired | name is missing |
400 InheritedTypeNotFound | baseType does not exist |
400 InheritedTypeIsSealed | baseType is sealed |
400 UserTypeCannotBeBothAbstractAndSealed | Both flags are set |
400 UserTypeInheritanceCycle | The chain loops back to this type |
400 SchemaValidationException, 400 FieldValidationException | A field definition is invalid, or a field is already declared by a base type |
409 ReservedUserTypeName, 409 ReservedUserTypeSlug | Base User / base-user |
409 UserTypeAlreadyExists | The slug is taken |
409 UniqueFieldHasDuplicates | A unique field has duplicate values among existing users |
Update a user type#
PUT /memberships/{membershipId}/user-types/{id}The body has the same fields as the create request and replaces the type: send all its own properties, not only the changed ones.
- Existing users are not rewritten. A new required field without a default value makes later updates of existing users fail until they get a value, so give new required fields a
defaultValueor fill them in first. - The slug of a type that is in use (by users or by derived types) can't change: a new slug is ignored, since users refer to their type by slug.
- An update without any change answers
409 IdenticalDocumentError.
Delete a user type#
DELETE /memberships/{membershipId}/user-types/{id}Response 204 No Content. A type that still has users or derived types can't be deleted: 400 UserTypeCanNotBeDelete.
Events#
UserTypeCreated, UserTypeUpdated and UserTypeDeleted. See Events.
Found a mistake in the docs? Open an issue