Location
A Location is a normalized postal address record shared across the financial graph. It captures a structured postal address (address lines, city, region, postal code, and ISO-3166-1 alpha-2 country code) that companies and people reference through pivot entities (CompanyLocation and PersonLocation). A single location row may be linked to multiple companies or people across a workspace, allowing address deduplication. The workspace association scopes it to a tenant and is nullable to allow global or pre-scoping rows.
| Naming | Value |
|---|---|
| Object | Location |
Resource type (JSON:API type) | location |
| Collection / records root | locations |
| REST base | /v1/locations |
| Entity class | Location |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/locations | ✅ Implemented |
| List (nested) | GET /v1/companies/{id}/locations · GET /v1/people/{id}/locations | ✅ Implemented |
| Retrieve | GET /v1/locations/{id} | ✅ Implemented |
| Create (nested) | POST /v1/companies/{id}/locations | ✅ Implemented |
| Update | PATCH /v1/locations/{id} | 🟡 Planned |
| Delete (nested) | DELETE /v1/companies/{id}/locations/{subId} | ✅ Implemented |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| location_id | string, UUID, 🔒 system | ✅ Yes | unique; DEFAULT gen_random_uuid() | — | Public stable identifier for this location. Generated server-side on creation. |
| full_address | string | ✅ Yes | no explicit length constraint | — | Denormalized single-line representation of the complete address. Used as the primary display string in the records table. |
| address_line1 | string | ✅ Yes | no explicit length constraint | — | Primary street address line (street number and street name). |
| address_line2 | string | ⚪ No | nullable; no explicit length constraint | — | Secondary address line for suite, floor, building, or apartment information. |
| city | string | ✅ Yes | no explicit length constraint | — | City or municipality of the address. |
| region | string | ⚪ No | nullable; no explicit length constraint | — | State, province, département, or region. Nullable because many EU addresses do not carry a region field. |
| postal_code | string | ⚪ No | nullable; no explicit length constraint | — | Postal or ZIP code. Nullable to accommodate countries or address forms that do not have postal codes. |
| country | string | ✅ Yes | varchar(2); ISO-3166-1 alpha-2 | Any valid ISO-3166-1 alpha-2 code (e.g. FR, US, DE, GB) | Two-letter country code. The 2-character length constraint is enforced at the database column level via @Property({ length: 2 }). |
| created_at | string (ISO 8601 datetime), 🔒 system | ✅ Yes | timestamptz; set on insert via onCreate lifecycle hook | — | Timestamp of when the location record was created. Set automatically by MikroORM on first persist. |
| updated_at | string (ISO 8601 datetime), 🔒 system | ⚪ No | timestamptz; set on insert and update via onCreate/onUpdate lifecycle hooks; not explicitly nullable at DB level | — | Timestamp of the last update to this location record. Maintained automatically by MikroORM. Declared as optional TypeScript type but not null in the database. |
| deleted_at | string (ISO 8601 datetime) | ⚪ No | timestamptz; nullable; null = not deleted | — | Soft-delete timestamp. When set, the location is considered deleted. All queries must filter deleted_at IS NULL. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| workspace | to-one (workspace) | ⚪ No | Optional tenant scope FK to core_api.workspaces (workspace_pk; ON UPDATE CASCADE ON DELETE SET NULL). Nullable to allow locations created before workspace scoping was retrofitted (Migration20260119180000). When present, restricts the location to the owning workspace. |
| company_locations | to-many (company_location) | — | Pivot entity CompanyLocation (core_api.company_locations) links this location to one or more companies. Each pivot carries is_primary, is_legal, label, created_at, and deleted_at. A partial unique index (uniq_company_locations_primary_company WHERE deleted_at IS NULL AND is_primary IS TRUE) enforces at most one primary location per company. |
| person_locations | to-many (person_location) | — | Pivot entity PersonLocation (core_api.person_locations) links this location to one or more people. Each pivot carries is_primary, is_legal, label, created_at, and deleted_at. A partial unique index (uniq_person_locations_primary_person WHERE deleted_at IS NULL AND is_primary IS TRUE) enforces at most one primary location per person. |
System-computed
- location_id is generated server-side via PostgreSQL DEFAULT gen_random_uuid() (Migration20260224100000). The column was added retroactively to all pre-existing rows via NOT NULL DEFAULT, backfilling historical rows on migration.
- created_at is set on INSERT by the MikroORM @Property({ onCreate: () => new Date() }) lifecycle hook. Not client-settable.
- updated_at is set on INSERT and on every UPDATE by the @Property({ onCreate/onUpdate }) lifecycle hooks. Not client-settable.
- deleted_at is the soft-delete sentinel. Null means active. Any non-null value means soft-deleted. All repository queries must include deleted_at: null.
- workspace FK (workspace_pk) was added in Migration20260119180000 with ON UPDATE CASCADE ON DELETE SET NULL. Locations created before workspace scoping have workspace_pk = NULL; this is a valid legacy state and not a data integrity issue.
- full_address is a denormalized convenience field written by the ingestion pipeline or enrichment service. It is not computed by a SQL function — it must be maintained by the writer at creation/update time.
- Bridge-table indexes (idx_company_locations_company_deleted, idx_company_locations_location, idx_person_locations_location, idx_person_locations_person) were added in Migration20260416100000 to support efficient bi-directional array-relationship traversals from Hasura.
- The primary_location Hasura computed field (Migration20251007135956) resolves the single active primary location for a company via a PostgreSQL function (core_api.company_primary_location) joining company_locations to locations WHERE is_primary = true AND deleted_at IS NULL. Exposed as issuer.primary_location and receiver.primary_location on invoices in overrides.yml.
Example
{"data": {"type": "location", "id": "7e3f9b24-14ac-4a5d-b8e1-c23d0f6a9107", "attributes": {"location_id": "7e3f9b24-14ac-4a5d-b8e1-c23d0f6a9107", "full_address": "12 Rue de la Paix, 75001 Paris, France", "address_line1": "12 Rue de la Paix", "address_line2": "3ème étage", "city": "Paris", "region": "Île-de-France", "postal_code": "75001", "country": "FR", "created_at": "2025-09-14T08:22:31.000Z", "updated_at": "2026-02-03T11:47:15.000Z", "deleted_at": null}, "relationships": {"workspace": {"data": {"type": "workspace", "id": "a1b2c3d4-0000-4abc-8def-112233445566"}}}}}apps/api/src/database/entities/Location.ts · domain: financial-graph · tier: Supporting