09 — User Behaviour (Users Module)
- 1. Discovery & search behaviour
- 2. Create / duplicate handling
- 3. Status-change behaviour matrix
- 4. Soft-delete behaviour
- 5. GDPR erasure behaviour
- 6. Bulk import behaviour
- 7. Self-service behaviour
- 8. Role & membership behaviour
- 9. Concurrency & multi-device
- 10. Error-behaviour policy (per 00-shared/06 §5)
Behavioural rules: how users act on these screens, what the system must do in response, and the guardrails that keep admin operations safe. Backend facts cited; behavioural rules are product policy consistent with the API.
1. Discovery & search behaviour
- Search is server-side and instant-debounced.
qmatches firstName, lastName, email, displayName case-insensitively (users.service.ts:92-99). Users type partial names — no leading wildcard cost concern for tenant scale; debounce 300 ms (00-shared/03 AppSearchBar). - Typing in search while scrolled to page 7 resets to page 1 — standard paginated-search semantics (00-shared/06 §3.2).
- Result count announced (live region) per 00-shared/09 §7.
2. Create / duplicate handling
- Duplicate email is the #1 real-world error (import + manual). Product rule: on 409 the UI offers "Search existing users" — since the email already belongs to someone, the fix is often "re-activate that user" instead of creating a new row.
- Email is stored lowercase (
user.schema.ts:28-29) — the client lowercases previews/display so users aren't surprised by case changes. - Phone duplicates are blocked per tenant (partial unique index,
user.schema.ts:84-90) — same inline treatment as email.
3. Status-change behaviour matrix
| From → To | System behaviour | UI note |
|---|---|---|
any → active | PATCH status | re-enables login for inactive/suspended users (login checks only isDeleted + lockout — auth.service.ts:124-141) |
→ inactive | visible in admin lists | copy: "can still sign in if credentials exist" (no server block) |
→ suspended | visible; login allowed (no suspension block in code — OQ-13) | copy must not overpromise a login block |
→ invited | no server support ((planned) invite flow) | option hidden until invite endpoint exists |
- Never present status changes as "security lockdown" — the backend enforces
only
isDeletedand account lockout (auth.service.ts:133-135).
4. Soft-delete behaviour
- Deleted users vanish from all scoped queries (
base.repository.ts:20-30) and cannot log in (users.repository.ts:21-25). - Audit trail survives (
UserDeleted→audit-write,event-queue-map.ts:12) — but no user-side "recently deleted" view exists; re-creating with the same email works after deletion (no tombstones checked on create — only live docs,users.service.ts:50-54). UI copy: "Re-adding the same email creates a new account." - Purge after 30 days (
tenant-purge.worker.ts:32-43) — the delete dialog states this.
5. GDPR erasure behaviour
- Erasure is immediate and irreversible at the UI level (anonymized PII,
users.service.ts:188-197; hard delete by job:200-209,tenant-purge.worker.ts:49-56). - Product rule: erasure requires the user's written request context; the
typed-confirm dialog is the consent gate. Erased emails become
erased-<id>@anonymized.invalid(users.service.ts:192) — never shown in lists (doc is soft-deleted same step). - No undo; no "erase" on active users without the delete step being visible in the same dialog.
6. Bulk import behaviour
- Partial success is the norm; the result screen is the contract of
truth (
imported+errors—users.service.ts:281). - Users fix files iteratively: error CSV → spreadsheet → re-upload; each
round rejects already-imported emails (
:260-263) — the wizard must make clear "remove previously imported rows or accept these errors". - Never auto-dedupe on the client — server is authoritative.
- Rows import with defaults (
Unknownnames,en,UTC—users.service.ts:265-271) — preview must surface "Unknown" fallbacks before import so admins can fix names. - Bulk-imported users get no
UserCreatedevent (:264-272callsrepo.createdirectly) → no welcome notification. Behavior rule: result screen states "Accounts created; no emails sent" ((planned)invite emails).
7. Self-service behaviour
- Users edit their own profile via
PATCH /users/:id— client must scope tosub(server self-guard(planned), OQ-3). - Preference toggles: instant feedback (optimistic) but always submitted
as the full merged object (full-replace semantics,
users.service.ts:161-163). - Theme
systemdefault mirrors OS; changing language is a client-side locale switch on save (server stores the preference only).
8. Role & membership behaviour
- Roles are managed on the membership, not the user
(
organization-member.schema.ts:21-25;rbac.service.ts:113-127). - Removing a member (
DELETE /rbac/members/:id,rbac.controller.ts:75-79) does not delete the user or revoke their tokens — role list in JWT goes stale until next login/refresh (JWT roles embedded at issue time —auth.service.ts:460-476). UI copy: "Role changes apply on next sign-in." org_adminself-demotion/self-removal: allow with confirmation (no server guard) — client warns "you may lose access".
9. Concurrency & multi-device
- Two admins editing the same user: last-write-wins with
versionbump (base.repository.ts:57-66) — no conflict error surfaced; the client refreshes from PATCH response so stale forms are overwritten. - Two admins creating the same email concurrently: both pass the pre-check,
one hits the unique index (
user.schema.ts:83) → 500 duplicate-key (not a clean 409 — OQ-14). Client treats 5xx with "try again"; server fix(planned). - List refresh after any mutation; no optimistic list-row removal except soft-delete (safe, idempotent, 00-shared/06 §3.5).
10. Error-behaviour policy (per 00-shared/06 §5)
| Code | Behaviour |
|---|---|
| 400 | field errors inline; import errors as result rows |
| 401 | silent refresh → session expiry flow |
| 403 | hide action; if reached → 403 screen |
| 404 | "User not found" empty state (never leak existence) |
| 409 | inline conflict + "search existing" affordance |
| 422 | (planned) reserved; not produced by users module today |
| 429 | backoff copy + no auto-retry |
| 5xx | generic + requestId; retry offered; note concurrency dupes (OQ-14) |