Skip to main content

Config Service — Data Model

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD


1. ID Prefix Registry

PrefixEntity
cfgn_ConfigNode
feat_FeatureDefinition
role_RoleDefinition
ri_RoleInheritance
grant_RoleFeatureGrant
ovr_UserNodeOverride
uid_UIDefinition
uir_UIVisibilityRule
tok_DesignToken

All IDs are ULIDs stored as text in PostgreSQL.


2. TypeScript Interfaces

// src/domain/config-node.ts
export type ConfigNodeId = Branded<string, 'ConfigNodeId'>;

export enum ConfigNodeType {
Global = 'GLOBAL',
Tenant = 'TENANT',
OrgNode = 'ORG_NODE',
Module = 'MODULE',
Feature = 'FEATURE',
Action = 'ACTION',
Role = 'ROLE',
User = 'USER',
UiScreen = 'UI_SCREEN',
UiComponent = 'UI_COMPONENT',
UiElement = 'UI_ELEMENT',
ActionBinding = 'ACTION_BINDING',
DesignSystem = 'DESIGN_SYSTEM',
}

export interface ConfigNode {
id: ConfigNodeId;
tenantId: TenantId | null;
nodeType: ConfigNodeType;
nodeKey: string;
parentId: ConfigNodeId | null;
payload: Record<string, unknown>;
scopeChain: ConfigNodeId[];
isActive: boolean;
version: number;
createdAt: Date;
updatedAt: Date;
}

// src/domain/feature-definition.ts
export type FeatureId = Branded<string, 'FeatureId'>;

export enum DataScopeType {
World = 'world',
Tenant = 'tenant',
NetworkOnly = 'networkOnly',
FacilityOnly = 'facilityOnly',
SameFacility = 'sameFacility',
Self = 'self',
}

export interface FeatureDefinition {
id: FeatureId;
tenantId: TenantId;
featureKey: string;
moduleKey: string;
allowedActions: string[];
dataScopeType: DataScopeType;
isActive: boolean;
}

// src/domain/role-definition.ts
export type RoleId = Branded<string, 'RoleId'>;

export interface RoleDefinition {
id: RoleId;
tenantId: TenantId | null;
roleKey: string;
displayName: string;
isAbstract: boolean;
isSystem: boolean;
}

export interface RoleInheritance {
id: string;
tenantId: TenantId;
childRoleId: RoleId;
parentRoleId: RoleId;
inheritanceType: 'full' | 'partial';
}

export interface RoleFeatureGrant {
id: string;
tenantId: TenantId;
roleId: RoleId;
featureKey: string;
grantedActions: string[];
deniedActions: string[];
}

// src/domain/user-node-override.ts
export type OverrideId = Branded<string, 'OverrideId'>;

export interface UserNodeOverride {
id: OverrideId;
tenantId: TenantId;
userId: UserId;
nodeId: ConfigNodeId;
featureKey: string;
action: string;
effect: 'allow' | 'deny';
justification: string;
effectiveFrom: Date;
effectiveTo: Date | null;
grantedBy: UserId;
}

// src/domain/design-token.ts
export type TokenId = Branded<string, 'TokenId'>;

export interface DesignToken {
id: TokenId;
tenantId: TenantId;
scopeNodeId: ConfigNodeId;
scopeType: 'global' | 'tenant' | 'module' | 'user';
tokenKey: string;
tokenValue: string;
locale: string | null;
}

3. PostgreSQL Schema

-- config_nodes: universal DAG node
CREATE TABLE config_nodes (
id TEXT PRIMARY KEY,
tenant_id TEXT,
node_type TEXT NOT NULL,
node_key TEXT NOT NULL,
parent_id TEXT REFERENCES config_nodes(id),
payload JSONB NOT NULL DEFAULT '{}',
scope_chain TEXT[] NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT true,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, node_type, node_key)
);

-- feature_definitions
CREATE TABLE feature_definitions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
feature_key TEXT NOT NULL,
module_key TEXT NOT NULL,
allowed_actions TEXT[] NOT NULL DEFAULT '{}',
data_scope_type TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, feature_key)
);

-- role_definitions
CREATE TABLE role_definitions (
id TEXT PRIMARY KEY,
tenant_id TEXT,
role_key TEXT NOT NULL,
display_name TEXT NOT NULL,
is_abstract BOOLEAN NOT NULL DEFAULT false,
is_system BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, role_key)
);

-- role_inheritance
CREATE TABLE role_inheritance (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
child_role_id TEXT NOT NULL REFERENCES role_definitions(id),
parent_role_id TEXT NOT NULL REFERENCES role_definitions(id),
inheritance_type TEXT NOT NULL DEFAULT 'full',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, child_role_id, parent_role_id)
);

-- role_feature_grants
CREATE TABLE role_feature_grants (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
role_id TEXT NOT NULL REFERENCES role_definitions(id),
feature_key TEXT NOT NULL,
granted_actions TEXT[] NOT NULL DEFAULT '{}',
denied_actions TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, role_id, feature_key)
);

-- user_node_overrides
CREATE TABLE user_node_overrides (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
user_id TEXT NOT NULL,
node_id TEXT NOT NULL,
feature_key TEXT NOT NULL,
action TEXT NOT NULL,
effect TEXT NOT NULL CHECK (effect IN ('allow','deny')),
justification TEXT NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
granted_by TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- ui_definitions
CREATE TABLE ui_definitions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
element_key TEXT NOT NULL,
element_type TEXT NOT NULL,
parent_element_id TEXT REFERENCES ui_definitions(id),
feature_key TEXT NOT NULL,
action_binding TEXT,
default_props JSONB NOT NULL DEFAULT '{"visible":true,"interactable":true}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, element_key)
);

-- ui_visibility_rules
CREATE TABLE ui_visibility_rules (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
element_id TEXT NOT NULL REFERENCES ui_definitions(id),
subject_type TEXT NOT NULL CHECK (subject_type IN ('role','user')),
subject_id TEXT NOT NULL,
is_visible BOOLEAN NOT NULL,
is_interactable BOOLEAN NOT NULL,
node_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- design_tokens
CREATE TABLE design_tokens (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
scope_node_id TEXT NOT NULL,
scope_type TEXT NOT NULL,
token_key TEXT NOT NULL,
token_value TEXT NOT NULL,
locale TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, scope_node_id, token_key, locale)
);

-- outbox (transactional outbox pattern)
CREATE TABLE outbox (
id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- inbox (deduplication for consumed events)
CREATE TABLE inbox (
event_id TEXT PRIMARY KEY,
source TEXT NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

4. Indexes

CREATE INDEX ix_config_nodes_tenant_id ON config_nodes(tenant_id);
CREATE INDEX ix_config_nodes_parent_id ON config_nodes(parent_id);
CREATE INDEX ix_config_nodes_node_type_active ON config_nodes(node_type, is_active);
CREATE INDEX ix_feature_definitions_tenant_module ON feature_definitions(tenant_id, module_key);
CREATE INDEX ix_role_definitions_tenant_key ON role_definitions(tenant_id, role_key);
CREATE INDEX ix_role_inheritance_child ON role_inheritance(tenant_id, child_role_id);
CREATE INDEX ix_role_feature_grants_role ON role_feature_grants(tenant_id, role_id);
CREATE INDEX ix_user_node_overrides_user_node ON user_node_overrides(tenant_id, user_id, node_id);
CREATE INDEX ix_user_node_overrides_effective ON user_node_overrides(effective_from, effective_to) WHERE is_active = true;
CREATE INDEX ix_ui_definitions_feature ON ui_definitions(tenant_id, feature_key);
CREATE INDEX ix_ui_visibility_rules_element ON ui_visibility_rules(tenant_id, element_id, subject_type);
CREATE INDEX ix_design_tokens_scope ON design_tokens(tenant_id, scope_type, token_key);
CREATE INDEX ix_outbox_undelivered ON outbox(created_at) WHERE delivered_at IS NULL;

5. Row-Level Security

-- Tenant isolation on all tables
ALTER TABLE config_nodes ENABLE ROW LEVEL SECURITY;
ALTER TABLE feature_definitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE role_definitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_node_overrides ENABLE ROW LEVEL SECURITY;
ALTER TABLE ui_definitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE design_tokens ENABLE ROW LEVEL SECURITY;

CREATE POLICY config_nodes_tenant_isolation ON config_nodes
USING (tenant_id = current_setting('app.current_tenant_id')
OR tenant_id IS NULL); -- GLOBAL nodes visible to all

CREATE POLICY feature_definitions_tenant_isolation ON feature_definitions
USING (tenant_id = current_setting('app.current_tenant_id'));

CREATE POLICY role_definitions_tenant_isolation ON role_definitions
USING (tenant_id = current_setting('app.current_tenant_id')
OR tenant_id IS NULL); -- system roles visible to all tenants

CREATE POLICY user_node_overrides_tenant_isolation ON user_node_overrides
USING (tenant_id = current_setting('app.current_tenant_id'));

6. Migrations

Migration files live in src/migrations/ using the naming convention:
YYYYMMDDHHMMSS_{description}.sql

Example: 20260418093000_add_config_nodes_table.sql

synchronize: false in Drizzle/ORM config — all schema changes via explicit migrations.