Appearance
Entity: userPreference
Entity Type: Database table (tenant schema)
Description: One personal setting of one user in one tenant, stored as a key and a structured value. It is the store behind the user preferences of the product: a setting that belongs to a person rather than to the organisation, follows that person across sessions and devices, and never changes what anybody else sees. The first setting kept here is the arrangement of an overview table — its visible columns and their order — under the key table.<TableViewKey>.columns; later personal settings (default screen after login, preferred chart units and similar) are further keys in the same table rather than further tables.
Per tenant, not per platform user. The same person can work for several customer organisations, and the columns an overview offers differ between them (modules, permissions, customer-specific attributes). A preference is therefore stored in the tenant schema, next to the data it arranges, so an arrangement made for one customer never leaks into another and is removed with the tenant.
One row per setting, value as JSON. A table arrangement is read and written as a whole, is small, and has no meaning outside its owner, so it is one JSON value rather than a row per column. The alternative — a child table with one row per column — would make every save a delete-and-reinsert of the set, and would add a join to every overview load for no query that needs it.
Also the home of showUiHints. GUI Labelling's field-hint icon toggle is stored the same way as every other personal setting on this table, under the key ui.showUiHints (see JSON-DAT: userPreference.showUiHintsValue). Because this table lives in the tenant schema, showUiHints is per-tenant like everything else here: a person working across two customer organisations can have hints on in one and off in the other. See GUI Labelling for the feature-level description.
Data Attributes Table
| Attribute Name | Description | Data Type | Default Value | Required (= Nullable) | Unique | Format | Validations | Index | Example |
|---|---|---|---|---|---|---|---|---|---|
| id | Primary key of the entity. | UUID | Generated in code (app layer) | Yes | Yes | UUID v7 | - | Primary Key | 018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21 |
| tenantId | Tenant the preference belongs to. Set by the storage layer from the request's tenant context; never passed by feature code. | UUID | - | Yes | No | UUID v7 | Must equal the current tenant (row-level security). | - | 018ed0b3-7a10-7c2e-8e61-2b7d1c3f4a55 |
| userId | The user whose preference this is. References the platform user by identifier only: users live in the platform database, so there is no cross-database foreign key. | UUID | - | Yes | Part of composite unique | UUID v7 | Always the authenticated user of the request; a user can never read or write another user's preference. | name: uq_user_preference_user_id_key, type: btree (unique) | 018ed0b3-1111-7c2e-8e61-2b7d1c3f4a55 |
| key | Name of the setting. Dotted, lower-camel segments; a table arrangement is table.<TableViewKey>.columns; the display-hints toggle is ui.showUiHints. | String | - | Yes | Part of composite unique | ^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)+$ | Max 100 characters. Only keys the backend knows are accepted: for table arrangements, the suffix must be a value of Enum - TableViewKey; other keys (like ui.showUiHints) are matched against a fixed allow-list maintained by the feature that owns them. | Part of the unique index above | table.gauges.columns |
| value | The setting itself. Its shape is fixed per key; for a table arrangement see JSON-DAT: userPreference.value, for the display-hints toggle see JSON-DAT: userPreference.showUiHintsValue. | JSONB | - | Yes | No | JSON object | Validated against the shape registered for the key; unknown shape is rejected. Max 16 KB. | - | {"version":1,"columns":[{"key":"name","visible":true}]} |
| createdAt | Timestamp of record creation. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 — YYYY-MM-DDTHH:mm:ss.SSSZ | Cannot be null. | - | 2026-09-16T10:00:00Z |
| updatedAt | Timestamp of the last update. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 — YYYY-MM-DDTHH:mm:ss.SSSZ | Cannot be null. | - | 2026-09-16T10:05:00Z |
| createdBy | Actor who created the record — always the owning user. | UUID | - | Yes | No | UUID v7 | Non-empty. | - | 018ed0b3-1111-7c2e-8e61-2b7d1c3f4a55 |
| updatedBy | Actor who last updated the record — always the owning user. | UUID | - | Yes | No | UUID v7 | Non-empty. | - | 018ed0b3-1111-7c2e-8e61-2b7d1c3f4a55 |
No deletedAt. Resetting a setting to its default removes the row: a preference has no history worth keeping, and a soft-deleted row would only have to be skipped by every read.
Indexes
| Name | Columns | Type | Why |
|---|---|---|---|
uq_user_preference_user_id_key | userId, key | Unique | One value per user per setting, and the only lookup path (a user loading one overview). Also makes a save an upsert rather than a read-then-write. |
Row-level security
Enabled and forced, with the tenant isolation policy every tenant-schema table uses (tenant_id = current_setting('app.tenant_id')::uuid, same WITH CHECK). Ownership by user is enforced by the application — every read and write is filtered by the authenticated userId — not by a database policy, because the database session knows the tenant but not the user.
Audited fields
None. A personal display setting changes nothing another user sees and nothing the organisation relies on, so it is not filed in the audit trail; the row's own updatedAt / updatedBy is enough to answer when it last changed.
Not registered for entityName resolution — not an audited entity; identified by its userId/key scope.
Lifecycle
- A row is created the first time a user confirms a change to a setting; a user who never changes anything has no rows and sees the defaults.
- Reset to default deletes the row.
- When a user loses access to the tenant, their rows are deleted with the membership removal; they carry no value to anybody else.