Multi-Entity Access · v0.9.1

Access Control

Three-scope user model: holding-wide auto-grant, multi-company, single-company. Time-restricted grants with expiry date and automated cleanup worker.

Holding-wide Multi-company Single-company Timed Access Grants AccessCleanupWorker migrations 101–102
Model A · Three Tiers

Holding-aware user scopes

Three tiers of access — from full-holding visibility down to a single subsidiary.

🏛️
Holding-wide
scope = holding

Owner, CFO, group controller. Automatic access to all companies in the holding — no explicit per-company rows needed.

Auto-grant via IsInHolding()
No user_company_access rows needed
New subsidiary → immediately visible
Controlled by users.holding_id
🏢
Multi-company
scope = multi_company

Accountant or manager serving several subsidiaries. Explicit rows in user_company_access for selected companies only.

Explicit rows per company
Each row has role (admin/accountant/manager/viewer)
Optional expires_at per row
New subsidiary NOT auto-visible
🏬
Single-company
scope = single_company

A manager, accountant or operator within one subsidiary. Exactly one access row. Cannot see or switch to other entities in the group.

Exactly 1 access row enforced
Company switcher hidden in UI
Default scope for new users
Can be temporary (expires_at)
Scope comparison — real scenarios
Scenario Scope Access rows Sees
Group owner / CFO holding None needed All 4 companies automatically
Shared accountant (2 subs) multi_company 2 rows (accountant) Only 2 assigned subsidiaries
Production manager single_company 1 row (manager) One subsidiary only
External auditor (30 days) single_company 1 row · expires 2026-08-01 One subsidiary · viewer only
UI · User Form

Company access matrix

The user creation and edit forms include a unified company access matrix tab — scope selector, per-company role assignment and optional expiry date on each row.

1

Select scope

Choose holding-wide, multi-company or single-company. Switching to single-company automatically clears all but one checkbox.

2

Assign per-company role

Each checked company gets a role: Admin · Accountant · Manager · Viewer. Role is stored per access row, not globally.

3

Set optional expiry

Leave empty for permanent access. Set a date for temporary grants — contractors, auditors, project-based access.

Компанії доступу Таб у формі редагування
Рівень доступу
🏢 Декілька компаній
Компанія Роль Доступ до
АРХ Груп ТОВ
Адмін
АРХ Агрохімія
Бухгалтер
АРХ Трейдинг
Бухгалтер
⏱ 2026-08-01
АРХ Сервіс
Automation

Timed Access Grants

Set expires_at on any user-company grant. 7-day warning notifications. Automated cleanup worker revokes expired access and invalidates the in-process cache.

Access lifecycle timeline
Grant
Day 0 · expires_at set
Active
Full access
Warning
T−7 days · notification
Expired
SQL filtered out immediately
Purge
Worker removes row
Active period Warning window (7 days) Expired (filtered from queries)
⚙️ AccessCleanupWorker
Grant Every 1 hour (configurable)
Step 1Find all expired rows FindExpired()
Step 2Revoke each row + invalidate in-process cache
Step 3Log warning for rows expiring within 7 days
DB schema — migrations 101 + 102
-- migration 101: users table additions
ALTER TABLE users
ADD scope user_scope DEFAULT 'single_company',
ADD user_type user_type_enum DEFAULT 'user',
ADD holding_id UUID → companies.id;
-- migration 102: timed access
ALTER TABLE user_company_access
ADD expires_at TIMESTAMPTZ,
ADD access_note TEXT;
-- partial index for cleanup worker
CREATE INDEX idx_uca_expires
ON user_company_access (expires_at)
WHERE expires_at IS NOT NULL;
-- constraint: note required when expiry set
ALTER TABLE user_company_access
ADD CONSTRAINT chk_uca_note
CHECK (expires_at IS NULL
OR access_note IS NOT NULL);
REST API

Access management endpoints

Method Endpoint Description
POST /users Create user with scope + company_assignments[]
GET /users/:id/company-access List user's access rows including expires_at
PUT /users/:id/company-access Replace all access rows atomically (RevokeAll + GrantBatch)
GET /my-companies Companies the authenticated user can access (scope-aware)
POST /auth/switch-company Set active company context in session. Validates access.
Related

Part of the Holding ecosystem