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.
Module: Platform Foundation (PF-02)
Audience: Organization administrators and platform admins
Last updated: 2026-04-30
This guide describes how users are added, invited, and removed from an
organization in Encore Health 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
- Open Settings → User Management → Invitations tab.
- Click Revoke on the row.
- Optionally enter a reason (recommended for compliance — examples: “sent
to wrong address”, “role change”, “no longer needed”).
- 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
- Open Settings → User Management → Invitations tab.
- 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.
| 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.