ErtisAuth

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:

FieldTypeRules
firstnamestringrequired
lastnamestring
usernamestringrequired, unique per membership
email_addressemailrequired, unique per membership
rolestringrequired
permissionsarray of stringunique items
forbiddenarray of stringunique items
user_typestringrequired
source_providerstringread-only
connected_accountsarray of objectsread-only
is_activebooleanread-only
membership_idstringread-only
sysobjectmanaged 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.

hierarchy
base-user (abstract)
└── person (abstract)       + phone, birth_date
    ├── customer            + loyalty_tier, addresses
    └── employee (sealed)   + department, employee_no

A type has all the fields of its ancestors plus its own.

FlagMeaning
isAbstractNo user can have this type; it only serves as a base for other types.
isSealedNo 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#

json
{
	"_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" }
}
FieldRequiredDescription
nameyesDisplay name. Base User is reserved.
slugnoDerived from the name when omitted. base-user is reserved. Users refer to their type by this slug.
descriptionno
baseTypenoSlug or name of the base type; stored as the slug. base-user when omitted.
isAbstract, isSealednoSee Inheritance. false by default.
allowAdditionalPropertiesnoWhen true, users may have fields that the schema does not declare. false by default: undeclared fields are rejected.
propertiesyesThe 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#

OptionTypeDescription
typestringThe field type (see below). Required.
displayNamestringHuman-readable name, for forms.
descriptionstringHelp text, for forms.
isRequiredbooleanThe field must have a value. For strings, whitespace-only values count as empty.
defaultValueanyValue used when the field is not given on creation.
isUniquebooleanNo two users may share a value (see Unique fields). Available on string, integer, float, boolean, enum and the string-based types.
isVirtualbooleanLets a derived type redeclare the field (see Redeclaring inherited fields).
isHidden, isReadonlybooleanHints for user interfaces. A hidden field that is required must have a defaultValue.
appearancestringHint for user interfaces (how to render the field).
isSearchable, searchWeightboolean, numberHints for search interfaces.
Note: 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

TypeValueOptions
stringtextminLength, maxLength, regexPattern (the value must match), restrictRegexPattern (the value must not match), formatPattern, caseInsensitive
integerwhole numberminimum, maximum (inclusive), exclusiveMinimum, exclusiveMaximum, multipleOf
floatnumberminimum, maximum, exclusiveMinimum, exclusiveMaximum
booleantrue / false
enumone of the itemsitems (required, unique, { "displayName", "value" }), isMultiple (the value is an array of item values)
consta fixed valuevalue, valueType
objectnested objectproperties (same format as the type's properties), allowAdditionalProperties
arraylistitemSchema (a field definition for the items), minCount, maxCount, uniqueItems, uniqueBy (field names that must be unique among object items)

Formatted types

TypeValueOptions
emailan email addressstring options
urian absolute URIstring options
hostnamea host namestring options
colora color code, e.g. #1E90FFstring options
dateyyyy-MM-ddminValue, maxValue
datetimeISO 8601 date and time, e.g. 2026-01-01T12:00:00ZminValue, maxValue
longtextmulti-line textstring options
richtextHTML textminWordCount, maxWordCount
codesource codestring options
jsonany JSON value
tagsarray of stringsminCount, maxCount, minLength, maxLength (of each tag)
location{ "latitude": 41.01, "longitude": 28.97 }
image, videomedia descriptorssize and dimension rules (maxSize, minWidth, maxWidth…)
referencea reference to other usersreferenceType (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:

json
"phone": {
	"type": "string",
	"displayName": "Phone",
	"maxLength": 20,
	"regexPattern": "^\\+?[0-9 ()-]{7,20}$"
}

A birth date that must be in the past century:

json
"birth_date": {
	"type": "date",
	"minValue": "1926-01-01"
}

A list of interests chosen from a set:

json
"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:

json
"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.
  • username and email_address are 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}.

MethodRouteDescriptionPermission
GET/user-types/{id}Get a user type (id or slug)user-types.read.{id}
GET/user-typesList the stored user typesuser-types.read
GET/user-types/allAll user types, base-user included, without paginationuser-types.read
GET/user-types/relations/{id}The declaring type of each fielduser-types.read.{id}
POST/user-types/_queryQuery user typesuser-types.read
POST/user-typesCreate a user typeuser-types.create
PUT/user-types/{id}Update a user typeuser-types.update.{id}
DELETE/user-types/{id}Delete a user typeuser-types.delete.{id}

Get a user type#

http
GET /memberships/{membershipId}/user-types/{id}

Returns the type with the fields inherited from its base types.

Field relations#

http
GET /memberships/{membershipId}/user-types/relations/customer

Groups the fields of a type by the type that declares them, up to base-user. Useful to build forms with one section per level:

json
{
	"base-user": [ "firstname", "lastname", "username", "email_address", "role", "…" ],
	"person": [ "phone", "birth_date" ],
	"customer": [ "loyalty_tier", "customer_no", "addresses" ]
}

Create a user type#

shell
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.

ErrorWhen
400 UserTypeNameRequiredname is missing
400 InheritedTypeNotFoundbaseType does not exist
400 InheritedTypeIsSealedbaseType is sealed
400 UserTypeCannotBeBothAbstractAndSealedBoth flags are set
400 UserTypeInheritanceCycleThe chain loops back to this type
400 SchemaValidationException, 400 FieldValidationExceptionA field definition is invalid, or a field is already declared by a base type
409 ReservedUserTypeName, 409 ReservedUserTypeSlugBase User / base-user
409 UserTypeAlreadyExistsThe slug is taken
409 UniqueFieldHasDuplicatesA unique field has duplicate values among existing users

Update a user type#

http
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 defaultValue or 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#

http
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