---
id: DOM-PFM-MTD-004
title: Custom Object
realises: CON-PFM-MTD-004
product_area: platform
module: metadata
revision: 1
status: draft
design_intent: confirmed
origin: Custom objects architecture D-01, D-05, D-08, D-09, D-13, D-16 (approved by Andrew 2026-10-18) and PLAT-M5
  D3, D7, D9, D10, D15, B2, B3, B4 (services-platform:docs/milestones/PLAT-M5-custom-records.md#Decisions; Product
  Accepted 2026-10-20)
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-PFM-MTD-custom-objects
readable_by:
- SVC-PFM-MTD-metadata
- SVC-PFM-MTD-layouts
- SVC-PFM-MTD-associations
lifecycle: STATE-PFM-MTD-003
custom_fields: none
identity: Stable keys identify the object in addresses and shared descriptions. Mutable settings are replaced at
  a version; explicit actions activate, archive and restore the definition.
attributes:
- name: key and object id
  kind: text
  required: true
  computed: false
  notes: Addresses use the object key; properties, presentations, queries and relationships use its record-type
    identity.
- name: labels, description and icon
  kind: text
  required: true
  computed: false
  notes: The object carries singular and plural labels, optional explanatory text and a console-supported icon.
- name: status
  kind: enum
  required: true
  computed: false
  notes: The definition moves among draft, active and archived through explicit lifecycle actions.
- name: naming
  kind: reference
  required: false
  computed: false
  notes: Activation needs a supported display property and permits up to three secondary properties; the configured
    prefix precedes record numbers.
- name: publication
  kind: enum
  required: true
  computed: false
  notes: Settings enable publication states and control public consumption of published records subject to property
    exposure.
- name: owner relationship
  kind: reference
  required: false
  computed: false
  notes: The owner type is active, points into this object and limits each record to one owner. Draft objects cannot
    be relationship endpoints, so ownership is attached after activation.
- name: staff access
  kind: enum
  required: true
  computed: false
  notes: Built-in mode accepts ordinary granted reach; roles-only mode additionally requires an assignment scoped
    to the object.
- name: portal access and portal modes
  kind: collection
  required: false
  computed: false
  notes: Stored portal rules can grant operations through ownership, organization roles, named links, publication
    or eligibility. Create and edit modes are validated, but record portal routes are absent.
- name: placement
  kind: reference
  required: false
  computed: false
  notes: Placement optionally names a console app or live custom app and a group. A separate legacy navigation setting
    is retained without reconciliation.
- name: limits
  kind: collection
  required: false
  computed: false
  notes: Per-owner record limits and per-record link limits are stored without enforcement.
- name: version, lock and modification facts
  kind: computed
  required: true
  computed: true
  notes: System facts include version and locks; mutations use the current version.
- name: custom app · definition
  kind: collection
  required: false
  computed: false
  notes: Custom apps have a permanent key, a label of at most one hundred characters, icon, order from zero to ten
    thousand and archival state. They are retained on archival and have no count cap.
- name: custom app · placed objects
  kind: computed
  required: false
  computed: true
  notes: App reads assemble their active placed objects, including grouping and staff-access settings.
relationships:
- to: DOM-PFM-MTD-002
  cardinality: one-to-many
  ownership: Metadata owns the properties stored as custom fields for this object's record-type identity.
  cross_service: false
- to: DOM-PFM-MTD-005
  cardinality: one-to-many
  ownership: Records reference their parent object, and the database prevents deletion of a parent with records.
  cross_service: false
- to: DOM-PFM-MTD-006
  cardinality: many-to-one
  ownership: The object selects its owner type; Associations prevents retirement while that selection remains.
  cross_service: false
- to: DOM-PFM-MTD-001
  cardinality: many-to-one
  ownership: The shared catalogue computes entries for draft and active custom objects without adding registry rows.
  cross_service: false
invariants:
- id: INV-DOM-PFM-MTD-004-01
  statement: Keys and record-type identities remain unique and unchanged across archival, and a workspace can hold
    fifty custom objects.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#refuses
    an object id the tenant already has,
- id: INV-DOM-PFM-MTD-004-02
  statement: Replacement cannot alter identity or status; lifecycle actions control status changes.
  enforced_at:
  - api
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/e2e/custom-objects.e2e.spec.js#answers 400 to a replacement
    that changes the status, which only the actions chang
- id: INV-DOM-PFM-MTD-004-03
  statement: Active definitions need an unarchived supported naming property and no more than three unarchived secondary
    properties.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#refuses
    activating a draft whose display prop
- id: INV-DOM-PFM-MTD-004-04
  statement: An owner relationship must be active, target this object and permit at most one owner per record.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#refuses
    an owner type that gives a record man
- id: INV-DOM-PFM-MTD-004-05
  statement: Publication cannot be disabled while any record remains published.
  enforced_at:
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#refuses
    turning publication off while the rec
- id: INV-DOM-PFM-MTD-004-06
  statement: Archived definitions reject replacement; restoration produces a draft requiring fresh activation.
  enforced_at:
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#restores
    an archived object as a draft, keepi
- id: INV-DOM-PFM-MTD-004-07
  statement: Placement accepts a console app or unarchived local custom app, and app archival refuses remaining
    draft or active placements.
  enforced_at:
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-apps.integration.spec.js#refuses
    to archive an app while a draft and an a
- id: INV-DOM-PFM-MTD-004-08
  statement: Object and app mutations audit changed field names without recording their values.
  enforced_at:
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/persistence/custom-objects.integration.spec.js#records
    the replacement with the fields it ch
- id: INV-DOM-PFM-MTD-004-09
  statement: Roles-only reach requires an active role assignment scoped to the object.
  enforced_at:
  - service
  provenance: test
  source: services-platform:modules/custom-objects/tests/e2e/custom-object-records.e2e.spec.js#keeps a roles-only
    object from a customer-wide assignment, answering as if
physical:
- platform.custom_objects
- platform.custom_apps
evidence:
- 'services-platform at ad3f6a65: modules/custom-objects/services/persistence/custom-objects.ts, custom-apps.ts,
  portal-access-rules.ts, custom-object-endpoints.ts; services/access/record-access.ts; db/custom-objects.table.ts,
  custom-apps.table.ts; placement-apps.ts; query-recipes.ts; rules.ts; docs/custom-objects.md §1 to §11 and §25'
- 'react-console at baf6dae3: lib/custom-objects/designer/save.ts, pending-owner.ts; components/configuration/custom-objects/custom-object-designer.tsx'
---

# Custom Object

## What It Models

A custom object gives an organization its own record category, such as a boat register. Its definition governs properties, names, lifecycle, access and optional placement; custom apps group categories in navigation.

## Identity and Attributes

Stable keys identify the object in addresses and shared descriptions. Mutable settings are replaced at a version; explicit actions activate, archive and restore the definition.

## Relationships

Metadata owns its property definitions, and custom records refer to the parent object. An optional protected relationship type supplies ownership.

## Invariants

Activation validates naming before ordinary records can be written. Identity limits, publication safeguards, placement checks and roles-only reach constrain later changes.

## How It Is Stored and Shared

Custom Objects writes object and app configuration. Catalogue, layout and relationship consumers read those definitions, and activation supplies shared Explorer recipes. Draft ownership choices currently remain in browser storage.

## Open Design Questions

Draft-owner persistence and unenforced stored limits need decisions. Reset also leaves retained definitions temporarily without their properties until reload succeeds.
