Relations

Relations & References

A relation property links a record to one or more other records. It is the single, unified way to model references in the platform — replacing the older foreignId field and the free-floating __links bag with one primitive.

Relation properties are backed entirely by the relations collection. The reference value is never stored inline on the record; it lives as an edge in relations, which means reverse lookup ("what points at me?") is a single indexed query from either side — no scanning of other collections.

Declaring a relation property

A relation property is a normal schema property whose x-type-schema uses the relation input/view/filter widgets. Its behaviour is controlled by a small configuration:

Option Effect
allowedTypeIds unset Any record of any type may be linked (the old "links bag" behaviour).
allowedTypeIds: ["companies"] Single-type reference (the old foreignId behaviour).
allowedTypeIds: ["contacts", "companies"] Polymorphic — any of the listed types.
type: "array" (minItems/maxItems) Many cardinality — a list of references. A non-array property is one (single reference).
inverse: { type, property } Opt-in bidirectional pairing (see below).
descriptions: { [typeKey]: InlineTranslation[] } Preset link descriptions offered when creating an edge (see below).

Cardinality lives on the property, never in the relations store — the store stays a simple edge list. allowedTypeIds holds bare type ids, not full tenant-<projectId>-<typeId> collection names.

Every record type can opt into a built-in links relation property — a many-cardinality bag with no allowedTypeIds by default, so any record can be linked. It is configured from the type settings page (Verlinkungen section: enable, allowed types, and description presets) which writes the property into the type's schemaOverrides. Templates and feature extensions declare it statically via buildLinksRelationProperty. This replaces the legacy injected __links bag; the edges it produces are tagged property: "links".

A relation property may define descriptions, a map keyed by target type (full tenant collection name or bare type id) to a list of InlineTranslation presets. When the configured target type is chosen in the input widget, a description <select> appears so the user can label the link (e.g. Owner, Real Estate Agent). The chosen string is stored on the edge's Link.description and rendered (italic) by the view widget.

How it is stored

Each link in a relations edge carries a property tag identifying which named property on the source record it satisfies:

{ links: [
    { collectionName: "contacts",  id: "ct1", property: "company" },
    { collectionName: "companies", id: "acme" }
] }

The tag is what lets two properties that target the same type — e.g. company and referredBy both pointing at companies — stay distinct. View and filter widgets fetch all edges for the record and filter by property at render time.

Display labels are always resolved live from the target's representationText; nothing is cached on the edge, so renaming a target record updates every reference automatically.

Viewing, editing and filtering

The relation property ships the full widget trio:

  • View — renders the linked records (property-scoped), resolving each label live.
  • Input — pick target records; allowedTypeIds restricts the selectable types, and single-cardinality replaces rather than appends.
  • Filter — filter a list by "records linked to this target via this property", resolved through the relations store.

Bidirectional relations (opt-in)

By default a relation is one-directional: if a Company declares an employees property pointing at Employees, the company shows its employees, and the employee side shows nothing.

To make it two-way, the schema author declares an inverse once:

Company.employees → inverse: { type: "<employees type>", property: "employer" }

Now linking a company to an employee also tags the employee-side edge as employer, so the employee's employer property shows the company. This is a design-time decision — end users never pick the reverse property per link. If only one side declares the relation, the other side simply shows nothing.

The unified relation property supersedes both older mechanisms:

  • foreignId — single-type inline references are replaced by a relation property with one allowedTypeIds entry.
  • __links — the free-floating bag is fully replaced by the links relation property (no allowedTypeIds). The runtime __links injection and the legacy linksConfiguration type field have been removed.

The __linkslinks cutover is complete: migration 027 converts any stored linksConfiguration into a links relation property in the type's schemaOverrides and removes the legacy field. The edges keep the property: "links" tag, so no edge data is reshaped.