> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# User Management Guide

> This guide describes how users are added, invited, and removed from an organization in Encore OS, and which audit fields are recorded for each lifecycle event.

**Last updated:** 2026-04-30

This guide describes how users are added, invited, and removed from an
organization in Encore OS, and which audit fields are recorded for
each lifecycle event.

***

## 1. The three ways to add a user

Settings → User Management exposes a single **Add User** menu with three
options. Pick the one that matches the situation:

| Option                                        | When to use                                                                                                | Result                                                                                                                                                              |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Invite by email** (recommended)             | The person does not yet have an Encore account.                                                            | Sends a branded invitation email with a magic link; row added to `pf_user_invitations` with status `pending`.                                                       |
| **Add existing Encore user**                  | The person already has an Encore profile (e.g. they belong to a sister organization on the same platform). | Sends a magic link and grants access immediately on first sign-in.                                                                                                  |
| **Create with temporary password** *(Legacy)* | Air-gapped onboarding where email cannot be used.                                                          | Creates the auth user with a temporary password the admin must convey out-of-band. **Discouraged** — prefer invite-by-email so the user sets their own credentials. |

The role catalog presented in all three flows comes from
`ASSIGNABLE_TENANT_ROLES` in `src/shared/lib/valid-roles.ts`. Legacy
module-specific roles (e.g. `hr_admin`, `fm_technician`) are intentionally
filtered out of this primary flow; they will be retired in a future release
(see Phase C of the user-management modernization plan).

***

## 2. Invitation lifecycle

```
 pending ──► accepted   (user signed in via magic link)
    │
    ├────► expired      (passed expiry date)
    │
    └────► revoked      (admin cancelled before acceptance)
```

The `pf_user_invitations` table tracks every transition. Rows are **never
hard-deleted**; this preserves the audit trail.

### Audit columns

| Column                       | Set when              | Purpose                                                                                                |
| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
| `status`                     | Always                | Current lifecycle state. CHECK constraint enforces one of `pending`, `accepted`, `expired`, `revoked`. |
| `expires_at`                 | At creation           | Default 7 days; refreshed on resend.                                                                   |
| `accepted_at`, `accepted_by` | Acceptance            | Records who actually signed in.                                                                        |
| `revoked_at`, `revoked_by`   | Revocation            | Records the admin who clicked Revoke.                                                                  |
| `revoked_reason`             | Revocation (optional) | Free-text reason captured in the Revoke dialog (≤ 500 chars).                                          |
| `resent_at`, `resend_count`  | Each resend           | Used to enforce rate limits.                                                                           |

***

## 3. Revoking a pending invitation

1. Open **Settings → User Management → Invitations** tab.
2. Click **Revoke** on the row.
3. Optionally enter a reason (recommended for compliance — examples: "sent
   to wrong address", "role change", "no longer needed").
4. Confirm.

The action calls the `revoke_invitation(_invitation_id, _reason)` RPC.
The RPC is a `SECURITY DEFINER` function and only succeeds when the caller
has admin access to the invitation's organization (enforced by RLS on the
underlying tables).

Revoked invitations remain visible in admin queries but cannot be accepted —
the magic-link flow rejects any token whose row is not in `pending` state.

***

## 4. Resending an invitation

1. Open **Settings → User Management → Invitations** tab.
2. Click **Resend** on the row.

Behind the scenes the `resend-invitation-email` Edge Function:

* Verifies the caller has org admin access.
* Refreshes `expires_at` (rolling 7-day window from the resend time).
* Increments `resend_count` and stamps `resent_at`.
* Re-sends the branded email through the shared `invitation-mailer.ts`.

### Rate limits

| Limit                                   | Default    | Behaviour when hit                                         |
| --------------------------------------- | ---------- | ---------------------------------------------------------- |
| Cooldown between resends per invitation | 60 seconds | Toast warning: "Please wait before resending again."       |
| Maximum resends per invitation per 24 h | 5          | Toast warning: "Resend limit reached for this invitation." |

These thresholds are applied inside the Edge Function and do not require
any client-side bookkeeping.

***

## 5. Permissions reference

| Action                            | Required role / permission                                                           |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| View user list                    | Org admin, site admin, or any user with `pf.users.view`                              |
| Invite by email                   | Org admin or site admin                                                              |
| Add existing user                 | Org admin or site admin                                                              |
| Create with temporary password    | Org admin (and the action is gated to be removed for non-platform admins in Phase B) |
| Revoke pending invitation         | Org admin or platform admin                                                          |
| Resend invitation                 | Org admin or platform admin                                                          |
| Manage roles for an existing user | Org admin or platform admin                                                          |
| Reset another user's password     | Org admin or platform admin                                                          |

Phase B of the modernization plan introduces dedicated permission keys
(`pf.users.invite`, `pf.users.create_direct`, `pf.users.revoke`) so admins
can delegate individual capabilities without granting full org admin.

***

## 6. Related code

| Concern                         | Location                                                    |
| ------------------------------- | ----------------------------------------------------------- |
| Page entry point                | `src/platform/users/UserManagement.tsx`                     |
| Invite dialog                   | `src/platform/users/InviteUserDialog.tsx`                   |
| Add-existing dialog             | `src/platform/users/AddExistingUserDialog.tsx`              |
| Legacy direct-create dialog     | `src/platform/users/CreateUserDirectDialog.tsx`             |
| Revoke confirm dialog           | `src/platform/users/RevokeInvitationDialog.tsx`             |
| Role catalog                    | `src/shared/lib/valid-roles.ts` (`ASSIGNABLE_TENANT_ROLES`) |
| Invitation mailer (shared)      | `supabase/functions/_shared/invitation-mailer.ts`           |
| Send invitation Edge Function   | `supabase/functions/send-invitation-email/index.ts`         |
| Resend invitation Edge Function | `supabase/functions/resend-invitation-email/index.ts`       |
| Revoke RPC                      | migration `20260430004002_*` (`public.revoke_invitation`)   |
| Lifecycle columns migration     | migration `20260429194049_*`                                |

***

## 7. Roadmap

The following improvements are tracked under the user-management
modernization plan:

* **Phase B (Medium-Term):** unified Add People dialog, server-side
  `pf_assignable_roles` table, role-permission preview, hardened route
  guards on `/settings/users`.
* **Phase C (Long-Term):** Postgres state machine
  (`pf_transition_invitation_status`), hourly `expire-invitations` cron,
  full `pf_audit_logs` integration, domain events
  (`user.invited`, `user.invitation_revoked`, `user.invitation_accepted`,
  `user.created`), retirement of legacy module-roles from
  `pf_user_role_assignments.system_role`, HMAC-signed invitation tokens.
