Start with the outcome, user, and evidence gap
Record the business outcome, affected users, current process, and measurable acceptance condition. Then locate exact capability evidence. Only a confirmed gap justifies a mechanism; missing evidence means more discovery, not permission to build.
A valid result can be no change: use an existing feature, configure it, or adapt the process deliberately. Mechanisms may be combined, but every extra layer widens ownership of code, testing, security, upgrades, and rollback.
Fields and entities change data, not an entire domain behavior
Custom fields and runtime entities help represent additional data. When the need includes behavior, cross-record validation, actions, integrations, background work, or its own lifecycle, a typed module or another extension seam may be required.
In the reviewed query layer, custom-field definitions and values are filtered by tenant_id, while field discovery and value joins do not automatically add organization_id. Virtual entities can apply organization scope. The project must therefore prove the intended data boundary instead of inferring it from the word “custom”.
App composition and app-local modules differ in scope but share ownership
When a change belongs to an app-owned page or section, compose it in the application. When it needs data, ACL, setup, services, jobs, APIs, and its own lifecycle, an @app module is full project-maintained code, not configuration.
A cross-module change should use an exact extension mechanism: the public inventory includes events, widgets, enrichers, interceptors, extension entities, API contracts, and portal slots. A family name alone does not prove that a specific surface fits.
An override replaces a stable ID, not arbitrary Core code
An application override works for explicitly supported kinds and stable identifiers. It can replace a definition or disable a supported contract with null. The documented resolution order runs from programmatic overrides through modules.ts and supported files to base registration; stale keys warn. A replacement must preserve the contract and have selection and collision tests. This is a precise mechanism, not a general patch.
Ejection and a fork are last-resort ownership levels
Ejection is limited to eligible modules declared ejectable. It copies that module into the application and changes its source to @app. Upstream no longer updates the copy automatically; its owner must track fixes, diff, merge, test, and maintain security. Consider ejection only after documented mechanisms are insufficient and a disable or return plan exists.
A maintained fork or Core patch is the red zone of permanent divergence. It needs explicit product and technical sponsors, budget, upstream watch, review of every release, security and regression tests, and an exit criterion. Without them, the decision should be rejected.
The decision covers the full lifecycle
For every code choice, record source, product and technical owners, tenant and data scope, permission model, contract and acceptance tests, observability, upgrade-review trigger, disable path, data migration, and rollback. The register below does not approve architecture; it blocks review-ready status until required fields are complete.
Customization mechanism map
| Mechanism | Suitable problem | Prerequisites | Exact evidence | Code and data ownership | Security and tenancy | Required tests | Upgrade review | Disable or rollback | Disqualifier | Source |
|---|---|---|---|---|---|---|---|---|---|---|
| Existing capability or process adaptationno code change | The verified product behavior or a deliberate process change already meets the outcome. | Exact capability evidence and an accepted process fit. | Capability matrix plus the cited module or documentation. | No new code owner; process owner owns adoption. | Verify roles, data boundaries, and process controls already in use. | Acceptance test for the real user outcome. | Recheck when the capability, process, or policy changes. | Revert configuration or process instruction. | The outcome still has a verified functional gap. | Inspect source |
| Configurationshipped configuration surface | A shipped option, workflow, role, definition, or policy expresses the required behavior. | Named configuration surface, ownership, and environment promotion path. | Exact configuration documentation or source. | Application/process owner owns values; platform retains underlying code. | Least privilege, safe defaults, tenant or organization behavior, and secret handling. | Configuration validation plus regression and promotion tests. | Recheck default changes and removed or renamed options. | Restore a versioned prior configuration. | The change needs behavior the configuration surface cannot express. | Inspect source |
| Custom field or runtime custom entityruntime data customization | Additional data is needed without a new coded domain lifecycle. | Field or entity definition, semantics, access, validation, and migration plan. | Data extensibility and custom-fields documentation. | Project owns definitions and data governance; platform owns the generic engine. | Prove tenant and organization scope; custom-field joins do not automatically add organization_id. | Definition, validation, permission, query, export, and migration tests. | Review field kinds, query behavior, identifiers, and schema evolution. | Disable use only with an explicit data retention, export, or deletion decision. | The need includes actions, cross-record rules, background work, or a coded lifecycle. | Inspect source |
| Direct application compositionapp-owned composition | An app-owned page, section, navigation, or presentation can compose public components and services. | The surface belongs to the application and requires no cross-module mutation. | App structure and module creation documentation. | Application team owns composition code and user experience. | Apply route access, scoped reads, safe rendering, and accessibility. | Page, permission, empty-state, interaction, and accessibility tests. | Review imported public contracts and component behavior. | Remove the route or component behind a documented flag. | The change alters another module or requires its lifecycle hooks. | Inspect source |
| App-local moduleproject-maintained module code | A cohesive app-owned capability needs data, ACL, setup, services, APIs, jobs, pages, or lifecycle. | A module boundary, stable IDs, owners, setup, and dependency design. | Create-first-module documentation. | The application team owns the complete @app module and every generated artifact. | Declare features, default-role policy, tenant/org scope, validation, and secret boundaries. | Unit, integration, ACL, migration, API, background, and acceptance tests as applicable. | Review every used contract and dependency at each upgrade. | Disable the module only with data, job, route, and dependency handling. | The change is only a small cross-module contribution to an existing surface. | Inspect source |
| Documented cross-module extensionpublic extension contract | A widget, event, enricher, interceptor, extension entity, API contract, portal slot, or other exact surface fits. | The precise surface, stable ID or event, absence behavior, and contract are verified. | Current extensibility-surface inventory and the cited implementation. | Extension owner owns contribution code; host module owns the public seam. | Enforce access and scope on every read/write; optional dependencies must fail safely. | Contract, collision/order, permissions, module-absence, failure, and acceptance tests. | Review the exact surface, ID, payload, ordering, and deprecations. | Unregister or feature-disable the contribution without breaking the host. | No documented surface matches the required lifecycle or placement. | Inspect source |
| Stable-ID application overridesupported replacement contract | A supported override kind must replace one exact registered implementation while preserving its contract. | Supported kind, exact stable ID, replacement contract, and collision policy. | App-level overrides documentation. | Application owns the replacement; platform owns discovery and base contract. | Replacement must preserve auth, scope, validation, redaction, and failure behavior. | Discovery, selection, collision, contract, permissions, and regression tests. | Review the override kind, stable ID, and base behavior on every upgrade. | Remove the override to restore the packaged implementation after data compatibility checks. | The target has no documented override kind or stable ID. | Inspect source |
| External integrationsystem boundary | The outcome depends on another system of record, provider, protocol, or remote side effect. | Named systems, direction, IDs, latency, reconciliation, outage, and exit behavior. | Integration guide plus the exact provider/API evidence. | Integration owner owns adapter and operations; each system keeps its domain ownership. | Credentials, authorization, tenant routing, minimization, replay, and vendor access need proof. | Contract, idempotency, retry, reconciliation, outage, security, and acceptance tests. | Review remote API, provider, schema, credentials, and platform integration seams. | Pause safely, drain work, reconcile, rotate/revoke access, and preserve exit data. | The behavior is entirely within one application boundary. | Inspect source |
| Ejected modulelast-resort copied ownership | A packaged module must be substantially owned and changed after all smaller documented seams are disproved. | Recorded alternatives, named owner, upstream watch, merge budget, data plan, and return/disable path. | Module ejection documentation. | The organization owns the copied @app module; upstream fixes no longer flow automatically. | Re-audit the full module, not only the edited lines. | Full module regression, security, migration, integration, performance, and acceptance tests. | Diff every relevant upstream release and merge fixes deliberately. | Restore packaged source only through a tested compatibility and data migration plan. | A configuration, extension, override, integration, or app module can meet the outcome. | Inspect source |
| Maintained fork or Core patchred-zone permanent divergence | A consciously permanent divergence remains after architecture review rejects every supported path. | Executive and technical sponsors, budget, legal/license review, upstream watch, release process, and exit criterion. | Pinned source plus compatibility and license evidence; no convenience command makes this safe. | The organization owns the divergent platform code and its full maintenance burden. | Full threat model and recurring security review across the changed platform boundary. | Complete affected-platform, compatibility, security, migration, performance, and application regression suites. | Review every upstream release and security advisory before any downstream release. | A funded convergence, replacement, or retirement plan with preserved data. | Missing sponsor, lifecycle budget, security ownership, or exit criterion means reject. | Project evidence and approval |
Fail-closed decision tree
- 1
Is the business outcome, affected user, and acceptance condition clear?
If yes: Continue to capability evidence.
If no: Stop: insufficient evidence; complete discovery before selecting a mechanism.
- 2
Does a verified current capability already meet the outcome?
If yes: Use the existing capability or adapt the process; record why no code is needed.
If no: Continue to configuration and data.
- 3
Can configuration or a runtime custom field/entity meet the gap?
If yes: Configure or use custom data; prove data scope and lifecycle limits.
If no: Continue to code ownership.
- 4
Is the change confined to an app-owned page, section, or cohesive local capability?
If yes: Compose it in the app or create an app-local module.
If no: Continue to a precise cross-module surface.
- 5
Does one exact documented extension surface match the placement and lifecycle?
If yes: Use that surface and test contract, permissions, ordering, absence, and failure.
If no: Pause for architecture review; a family name alone is insufficient.
- 6
Must one supported stable-ID implementation be replaced?
If yes: Use an application override that preserves the documented contract.
If no: Continue to system-boundary analysis.
- 7
Does the outcome depend on an external system or provider?
If yes: Design an integration with ownership, reconciliation, outage, and exit behavior.
If no: Continue only if the need remains inside the packaged module.
- 8
Have configuration, app code, modules, extensions, overrides, and integrations been evidenced as insufficient?
If yes: Architecture review may consider ejection as a last resort.
If no: Reject ejection and return to the smallest supported mechanism.
- 9
Is permanent platform divergence explicitly required and funded?
If yes: Escalate to red-zone fork/patch review with sponsors, security ownership, upstream watch, and exit criterion.
If no: Do not fork or patch Core.
- 10
Are source, owners, scope, security, tests, upgrade trigger, disable path, rollback, rationale, approver, and review date complete?
If yes: Record the decision as review-ready; an authorized human still approves it.
If no: Keep the decision not ready or rejected.
Lifecycle questions for every code choice
- What exact public source and stable contract justify this mechanism?
- Who owns the product outcome, technical design, implementation, maintenance, supplier dependency, and team continuity?
- Which tenants, organizations, users, records, and environments are in scope?
- How are authorization, validation, privacy, encryption, secrets, and audit behavior proven?
- Who owns data definitions, migrations, retention, export, deletion, and reconciliation?
- What happens on dependency absence, partial failure, retry, duplication, timeout, and degraded operation?
- Which contract, permission, migration, integration, accessibility, performance, and acceptance tests are required?
- Which logs, metrics, alerts, audit records, technical documentation, and support runbooks expose failure?
- Which exact contract, dependency, ID, generated artifact, or upstream module triggers upgrade review?
- Can the change be disabled without breaking data, jobs, routes, integrations, or users?
- What code, schema, data, configuration, external-side-effect, and supplier rollback is possible?
- What review date, expiry, convergence, replacement, or retirement criterion ends this choice?
Local planning tool
Customization decision register
All 13 starter areas begin with insufficient evidence. Complete every ownership and lifecycle field before recording approval; a rejected route needs a complete rationale and approver record.
Privacy: Entries stay in this browser and local exports. Do not enter secrets, credentials, personal data, or production records.
Register state
NOT READYOpen gates: 286
| Area | Business outcome | Affected users and process | Current capability evidence | Verified gap | Decision state | Candidate mechanism | Exact source or contract | Alternatives considered | Data, tenant, and organization scope | Security and privacy controls | Product owner | Technical owner | Implementation owner | Maintenance owner | Acceptance and technical tests | Upgrade review trigger | Disable and rollback path | Dependencies | Status | Decision rationale | Approver role or name | Review date |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Additional data attribute | ||||||||||||||||||||||
| Additional data entity or relationship | ||||||||||||||||||||||
| App-owned page, section, or navigation | ||||||||||||||||||||||
| Cross-module UI contribution | ||||||||||||||||||||||
| Validation, permission, or mutation guard | ||||||||||||||||||||||
| API route or contract | ||||||||||||||||||||||
| Event, subscriber, queue, or background job | ||||||||||||||||||||||
| Workflow or business-rule behavior | ||||||||||||||||||||||
| External integration or synchronization | ||||||||||||||||||||||
| Security, privacy, identity, or audit control | ||||||||||||||||||||||
| Reporting, search, or indexing behavior | ||||||||||||||||||||||
| Whole-module behavior divergence | ||||||||||||||||||||||
| Permanent Core or platform divergence |
Method, assumptions and limitations
Reviewed 14 July 2026. Product facts were checked against both the public code at the cited repository revision and official documentation. Where documentation and code differ, this guide describes behavior supported by code. Interpretations and recommendations concern implementation work, not product guarantees.
This material is not a quote, audit, certification, or legal, tax, or accounting advice. Edition, enabled modules, configuration, custom code, infrastructure, data, third-party providers, and operating practices affect the outcome.
Primary source collections: code repository, documentation, public releases.