---
title: Relations & References
feature: true
featureGroup: data
featureState: beta
description: Link records together with typed, queryable relation properties backed by a single relations store
date: 2026-06-05
tags:
  - relations
  - references
  - links
  - foreign-id
  - data-modeling
---

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

### The free-floating `links` property

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"`.

### Per-link descriptions

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.

## Relationship to `foreignId` and `__links`

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 `__links` → `links` 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.
