1. Purpose
A User is a person with access to oxFlow. Users are synced from Microsoft 365 (the identity source of truth) — they cannot be created directly in oxFlow. M365 brings identity (name, email, M365 ID); oxFlow assigns access via a Role (Admin / Lead Estimator / Estimator) post-sync.
The split is deliberate: IT manages identity (M365); oxFlow manages access (Role). Adding or removing people happens in M365; reassigning what they can do happens in oxFlow’s Admin page.
Analogy (glossary cooking model): the User is the kitchen staff — IT decides who’s on payroll; the head chef (Admin) decides who works the line versus the prep station.
2. Attributes
| Attribute | Type | Required | Default | Notes |
|---|---|---|---|---|
id | UUID | ✅ | generated | System-managed |
name | string (text) | ✅ | — | Display name as synced from M365 (e.g., “Alice Brown”) |
email | string (email) | ✅ | — | M365 primary email; used for login + display. Unique workspace-wide |
m365_id | string (unique) | ✅ | — | M365 object ID (immutable). Anchors the sync; prevents duplicates |
role_id | Role ref | ✅ | — | Assigned Role: Admin · Lead Estimator · Estimator (BR-084) |
is_active | boolean | ✅ | true | Mirrors M365 enabled status; falsified when M365 disables/deletes the account |
last_login_at | timestamp | ❌ | null | Last successful login; populated by auth subsystem |
created_at / updated_at | timestamp | ✅ | system | Audit. created_by is always system (M365 sync); updated_by tracks Role changes in oxFlow |
created_by / updated_by | User ref | ✅ | system | Audit |
Derived attributes (computed, not stored) — see §8.
3. Roles
Every User has exactly one Role (BR-084). Multi-role Users are deferred to v2.
| Role | Scope | Summary |
|---|---|---|
| Admin 🟢 | Full workspace | System owner. Manages Users, Roles, lookups, branding, integrations |
| Lead Estimator 🟢 | Tender / Estimate control | Creates Tenders/Estimates, manages Commercials, runs Adjudications, locks/submits |
| Estimator 🟢 | Item-level work | Edits Items, Worksheets, Worksheet Resources, Variables, Calculations, Content Blocks |
Roles are hierarchical — Admin ⊇ Lead Estimator ⊇ Estimator. If a capability is granted to Estimator, the higher Roles automatically have it.
Full capability matrix: see roles-permissions.md §3.
Role assignment authority (BR-085, BR-086):
- Only Admins can assign or change Roles
- Role assignment happens in oxFlow’s Admin page after M365 sync brings in identity
- Default Role on first sync: Estimator (lowest privilege; Admin elevates as needed)
4. Lifecycle States & Sync
Users have no lifecycle state machine beyond is_active. The sync mechanics are:
Sync direction: M365 → oxFlow only. oxFlow does not push User changes back to M365.
Sync triggers:
- Scheduled — periodic background sync (cadence configurable; admin-managed)
- On-demand — Admin clicks “Sync M365 Users” in Admin page
- On login — first-time login from a new M365 account triggers a sync record (Estimator default)
What sync brings in:
name,email,m365_id,is_active— sourced from M365 every sync- New Users are added with Role = Estimator (default); Admin elevates as needed
- Removed/disabled M365 accounts →
is_active = false(User remains for audit/history but cannot log in)
What sync does NOT touch:
role_id— Role assignment is oxFlow-only. M365 group membership does not influence Role- Audit trail — historical references to a User remain even after deactivation
Re-activation: if a previously deactivated M365 account is re-enabled, the next sync flips is_active back to true. Role is preserved (was held throughout deactivation).
5. Relationships
Inbound (things referring to User)
| From | Cardinality | Role context | Notes |
|---|---|---|---|
| Estimate | Estimate M:1 User | Lead Estimator | Required (BR-093); nominal accountability, not access gate |
| Item | Item M:0..1 User | Assigned Estimator | Optional informational tag (BR-095) |
| Audit log entries | many M:1 User | Various | All write actions stamp updated_by |
Outbound (things User references)
| To | Cardinality | Required | Notes |
|---|---|---|---|
| Role | User M:1 Role | ✅ | Exactly one Role per User (BR-084) |
6. Validation / Invariants
Rules that must hold at all times:
- M365 ID unique. No two Users may share the same
m365_id. Enforced at sync time. - Email unique. No two Users may share the same
email(M365 enforces this upstream; oxFlow re-validates). - Exactly one Role.
role_idcannot be null; multi-role assignment is rejected (BR-084). - Immutable M365 ID. Once synced,
m365_idcannot be changed or transferred. - Cannot delete with references. A User with inbound references (Lead Estimator on Estimates, audit log entries) cannot be hard-deleted; deactivation only.
- Role enum.
role_idmust reference a valid Role (Admin / Lead Estimator / Estimator). No other values. - Inactive User cannot perform writes.
is_active = falseblocks all write actions; read access is admin-configurable.
7. Derived / Computed Attributes
| Attribute | Derivation | Notes |
|---|---|---|
is_admin | role_id references Admin Role | Convenience flag for UI gating |
is_lead_estimator_or_above | Role ∈ {Admin, Lead Estimator} | Convenience flag for capability checks |
assigned_estimate_count | Count of Estimates where lead_estimator_id = this.id | For workload reporting |
assigned_item_count | Count of Items where estimator_id = this.id | For workload reporting |
8. Worked Examples
Example A — New User from M365 sync (default Estimator role)
A new hire is added to M365’s “oxFlow Users” group. Next scheduled sync:
User (created by sync):
name: "Charlie Davis"
email: "charlie.davis@oxcon.local"
m365_id: "M365-7F2E-..."
role_id: <Estimator Role> ← default on first sync
is_active: true
created_by: system (M365 sync)
created_at: 2026-04-15T08:30:00Z
Admin then logs in, navigates to Admin → Users, sees Charlie listed,
elevates Role to "Lead Estimator" because Charlie is replacing a departing Lead.
Updated:
role_id: <Lead Estimator Role>
updated_by: admin@oxcon.local
updated_at: 2026-04-15T09:12:00Z
Example B — User deactivated in M365
An employee leaves; their M365 account is disabled. Next sync:
User (before sync):
name: "Bob Roberts"
email: "bob.roberts@oxcon.local"
is_active: true
role_id: <Lead Estimator>
assigned_estimate_count: 4
Sync detects M365 status = disabled →
User (after sync):
is_active: false
role_id: <Lead Estimator> ← preserved for audit
Effects:
- Bob can no longer log in
- Estimates Bob led remain assigned to him (audit trail preserved)
- Lead Estimator field on those Estimates still shows "Bob Roberts"
(any Lead Estimator can still act on them per the flat model — see BR-093)
- Items Bob was assigned (estimator_id) remain tagged for history
Example C — Role change preserves audit history
Admin demotes a Lead Estimator back to Estimator (e.g., role rotation):
Before:
User "Alice Brown"
role_id: <Lead Estimator>
assigned_estimate_count: 7
After Admin role change:
role_id: <Estimator>
updated_by: admin@oxcon.local
Effects:
- Alice's Lead Estimator capabilities are revoked immediately (next request)
- Estimates where Alice is the named Lead Estimator: lead_estimator_id remains "Alice"
(the field is nominal, not an access gate per BR-093)
- Admin should reassign Lead Estimator on those Estimates if they want
accountability to follow the Role change
- Audit log preserves the role change with timestamp + acting Admin