Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Platform

Explanation

Tenant fields

How core/tenant_fields.py and server/routes/tenant_fields.py let an administrator switch shipped fields off and add tenant-defined fields stored in an extra JSONB bag.

Tenant fields let each tenant decide what its forms collect, without code changes: shipped fields can be switched off or made required, and new fields can be added. This page explains the core service in src/cluster_erp/core/tenant_fields.py and the routes in src/cluster_erp/server/routes/tenant_fields.py. The metadata classes it builds on are listed on metadata model.

In short

  • An entity opts in by declaring EntityMeta.tenant_fields with one or more storage buckets (src/cluster_erp/core/metadata.py:517-550). 25 metadata declarations do so (counted with grep -rn "tenant_fields=TenantFieldsMeta" over src/cluster_erp/modules).
  • Three tables hold the tenant's decisions: core_field_catalog, core_field_definitions, and core_field_settings (src/cluster_erp/core/tenant_fields.py:168-295).
  • A tenant field's value lives in the extra JSONB column of the bucket row, as extra[key] (src/cluster_erp/core/tenant_fields.py:16-19, src/cluster_erp/core/metadata.py:142-146).
  • Clients apply a per-tenant overlay from GET /api/v1/_overlay on top of the fixed metadata (src/cluster_erp/server/routes/tenant_fields.py:446-456).
  • The settings screens are generated: the service injects settings entities into the declaring module's metadata (src/cluster_erp/core/tenant_fields.py:3433-3497).

How it works

The three tables

Table Class What a row means Source
core_field_catalog FieldCatalogEntry One concept the tenant collects, defined once: code, labels, type, choices, category, protection class, permission src/cluster_erp/core/tenant_fields.py:168-201
core_field_definitions FieldDefinition One placement of a concept on an entity bucket: own (stored on the row) or derived (shown read-only from a linked record) src/cluster_erp/core/tenant_fields.py:208-266
core_field_settings FieldSetting The tenant's decision on one shipped field: enabled, required override, order, list and filter flags src/cluster_erp/core/tenant_fields.py:269-292

All three derive from BaseModel, so they are deactivated, not deleted, and their unique indexes cover live rows only (src/cluster_erp/core/tenant_fields.py:194-201, src/cluster_erp/core/tenant_fields.py:255-266, src/cluster_erp/core/tenant_fields.py:287-295). Closing a definition sets is_active to false; stored values stay and are shown read-only (src/cluster_erp/core/tenant_fields.py:9-13).

Specs from metadata

At start-up the server calls build_specs with every module's metadata (src/cluster_erp/server/app.py:151). The specs record each declaring entity, its buckets, the shipped fields of each bucket, and the foreign-key links between entities (src/cluster_erp/core/tenant_fields.py:300-354, src/cluster_erp/core/tenant_fields.py:451-500). bucket_for_table maps a table name back to an entity and bucket; the change history uses it (src/cluster_erp/core/tenant_fields.py:506-513, src/cluster_erp/core/change_history.py:373).

Values

Owning services call TenantFieldService.validate_extra before they store a row. Every key must be a known, open field of the bucket, and every value is coerced by its definition. A closed field keeps its stored value but takes no new value; an unknown key is refused (src/cluster_erp/core/tenant_fields.py:1942-1955). coerce_value handles the eight field types and stores a decimal as a string (src/cluster_erp/core/tenant_fields.py:139-150, src/cluster_erp/core/tenant_fields.py:854-911). Violations raise FIELD_VALUE_INVALID (src/cluster_erp/core/errors.py:61-66).

Overlay and settings screens

The flowchart shows how a tenant's decisions reach a client.

flowchart LR
 meta["Module metadata: tenant_fields buckets"] --> specs["build_specs at start-up"]
 specs --> inject["with_tenant_field_entities"]
 inject --> metaapi["GET /api/v1/_meta"]
 tables["core_field_catalog, definitions, settings"] --> overlay["TenantFieldService.overlay"]
 specs --> overlay
 overlay --> overlayapi["GET /api/v1/_overlay"]
 metaapi --> client["Web and desktop clients"]
 overlayapi --> client

In prose: the metadata document stays one per process and carries the settings entities tenant_field_setting, tenant_field_definition, and tenant_field_catalog, added to the declaring module (src/cluster_erp/core/tenant_fields.py:3430-3474). A module without a settings section gets one (src/cluster_erp/core/tenant_fields.py:3484-3497). The overlay is computed per request from the tenant's tables: fields to add, shipped fields to prune, and closed fields to show read-only (src/cluster_erp/server/routes/tenant_fields.py:446-456).

Routes

The settings routes live under each declaring module, /api/v1/{module}/_fields/..., so the generic list and form engines reach them like any other endpoint (src/cluster_erp/server/routes/tenant_fields.py:1-9, src/cluster_erp/server/routes/tenant_fields.py:42). There are 18 routes: definitions (list, create, get, update, close, reopen), settings (list, get, update), catalog (list, create, get, update, bind, derive, close, reopen), and the overlay (counted from the @router. and @overlay_router. decorators, src/cluster_erp/server/routes/tenant_fields.py:150-456). Each settings route requires a permission named after its entity, for example tenant_field_definition.read (src/cluster_erp/server/routes/tenant_fields.py:155).

Search and history

When a field becomes searchable, the service builds a trigram expression index on extra->>'key' concurrently, using the same normalised expression as the text search (src/cluster_erp/core/tenant_fields.py:1681-1690). See search, pagination, and bulk operations. The change history records extra.<key> like a column, redacts keys of protection class 3 and above, and emits TenantFieldValuesChanged after the commit (src/cluster_erp/core/tenant_fields.py:916-922, src/cluster_erp/core/change_history.py:137-151, src/cluster_erp/core/change_history.py:357-386).

Why it is built this way

  • The module docstring states the design: an entity only declares its buckets in metadata, and everything else is platform work in this module, so modules never import one another (src/cluster_erp/core/tenant_fields.py:1-31).
  • The catalog docstring states why concepts are defined once: every placement shares the label, choices, and protection, so the same thing is never two fields with two spellings (src/cluster_erp/core/tenant_fields.py:169-174).

Rules and boundaries

  • A field key must match ^[a-z][a-z0-9_]{1,59}$ and must not be a shipped column (src/cluster_erp/core/tenant_fields.py:152-153).
  • Each category has a minimum protection class; a field may be protected more, never less (src/cluster_erp/core/tenant_fields.py:86-101).
  • From protection class 3, a field needs a permission gate (src/cluster_erp/core/tenant_fields.py:154-155). Field-level access is documented on field and row access.
  • A new field that duplicates an existing concept is refused with FIELD_DEFINITION_DUPLICATE (src/cluster_erp/core/errors.py:67-70).

Interaction with other parts

  • Owning services in 15 modules use the service (counted with grep -rln for TenantFieldService, validate_extra, or tenant_fields import over src/cluster_erp/modules).
  • Modules declare which shipped fields other code depends on by answering DescribeFieldConsumers (src/cluster_erp/core/contracts.py:2470). See contracts reference.

Limits and open points

  • An index build failure is logged, not raised; search on that field works without the index until the next attempt (src/cluster_erp/core/tenant_fields.py:1684-1687).

Terms introduced

Term Code identifier Definition Turkish term Source
tenant field FieldDefinition A tenant-defined field placed on an entity bucket. kiracı alanı src/cluster_erp/core/tenant_fields.py:208
field catalogue FieldCatalogEntry The tenant's list of field concepts, each defined once. alan kataloğu src/cluster_erp/core/tenant_fields.py:168
bucket TenantBucketMeta A row type that stores tenant field values in its extra column. kova src/cluster_erp/core/metadata.py:517
overlay overlay The per-tenant changes the clients apply to the fixed metadata. katman src/cluster_erp/core/tenant_fields.py:1856
protection class protection_class A level from K0 to K4 that says how sensitive a field's values are. koruma sınıfı src/cluster_erp/core/tenant_fields.py:129-137

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13