Every link between two records carries a typed relationship — the catalog below is what each tenant ships with by default. Tenants can rename or extend any of these from the CRM admin; the APP picker stays in step via the per-tenant catalog cache.
A relationship type defines an asymmetric pair — one entity type sits on the left side, the other on the right. display_name is what the link reads when viewed from the left; inverse_display_name is what it reads when viewed from the right.
The mobile APP and the CRM share the same catalog, served by GET /api/v1/relationship-types. The APP caches the response per tenant in relationship_types_cache so the link picker can render the correct directional label offline. When the cache hasn't hydrated yet the picker falls back to a hardcoded seed that mirrors the default catalog.
RELATED_TO is always offered as a generic fallback — if no catalog row matches a given pair, the link still goes through and the server records it as a typed catch-all.
any as the left-hand entity type: ASSIGNED_TO and PINNED_BY. any means "matches any entity type on this side" — useful for relationships that aren't tied to a specific kind of record (assigning a person to an asset, an event, an order … or pinning anything).
Seeded by DefaultRelationshipTypeSeeder for every new tenant. is_system = true, is_active = true.
| System code | Forward label | Inverse label | Pair (left → right) | Dashboard signal |
|---|---|---|---|---|
WORKS_AT |
Works At | Has Worker | person → organisation | No |
OWNS |
Owns | Owned By | person → asset | Yes |
ASSIGNED_TO |
Assigned To | Assigned | any → person | Yes |
MEMBER_OF |
Member Of | Has Member | person → organisation | No |
ATTENDED |
Attended | Attended By | person → event | Yes |
REPORTED_BY |
Reported By | Reported | incident → person | Yes |
INVOLVED_IN |
Involved In | Involves | person → incident | Yes |
INSPECTED_BY |
Inspected By | Inspected | inspection → person | Yes |
ASSET_OF |
Asset Of | Has Asset | asset → organisation | No |
PINNED_BY |
Pinned By | Has Pinned | any → person | Yes |
VISITED_AT |
Visited At | Had Visit | event → organisation | Yes |
database/seeders/DefaultRelationshipTypeSeeder.php
These come from CrmDefaultsSeeder and earlier APP releases. They cover the order & cross-link flows that aren't in the dashboard-focused default seeder. The APP keeps them in its offline-fallback seed so the picker still works for orders / events / parent-org links before the cache hydrates.
| System code | Forward label | Inverse label | Pair (left → right) |
|---|---|---|---|
ORDERED_BY |
Ordered By | Placed Order | order → person |
ORDERED_FROM |
Ordered From | Has Order | order → organisation |
ORDER_FOR_ASSET |
Order For Asset | Asset Has Order | order → asset |
ORDER_FOR_EVENT |
Order For Event | Event Has Order | order → event |
ASSET_AT_EVENT |
Asset At Event | Event Has Asset | asset → event |
PARENT_ORG |
Parent Org | Subsidiary | organisation → organisation |
RELATED_TO |
Related To | Related To | any → any (fallback) |
CrmDefaultsSeeder.php + APP lib/utils/constants.dart
A relationship type with is_dashboard_signal = true shows up on a person's dashboard as work-on-their-plate — Assigned To, Reported By, Inspected By, etc. Identity-context links like Member Of and Works At stay quiet so the dashboard isn't drowned by org-chart links.
The flag is read by RelationshipType::scopeDashboardSignal() on the API and consumed by the dashboard payload (GET /api/v1/me/dashboard).
Tenants can rename, deactivate, or add their own relationship types in the CRM admin (Filament). The APP picker reflects custom labels on the next sync — the catalog refreshes at boot, after a workspace switch, and via pull-to-refresh on the dashboard.
The wire payload is unchanged: the APP sends the system_code string, and the server's smart matcher resolves it against system_code, name, or display_name — case-insensitive — before falling back to entity-type pair inference.