Skip to main content

Screen: Users

web/users.html. Lists the people in the organization, shows which of them can actually sign in, invites new internal teammates, and lets an administrator change a person's role. Admin-only.

Roster and access are two different facts​

A people row makes someone addressable — they can own work, appear in the spine, and be named in audit history. It does not give them a login. Signing in additionally requires a Supabase Auth identity linked through people.auth_user_id; without that link, a person who authenticates is "authenticated but not a provisioned person" and every API returns 401. See security posture.

The Sign-in access column reports which of the two a person has:

StateMeaning
ActiveInvitation accepted. This person can sign in.
InvitedAn identity exists; the invitation has not been accepted yet.
No accessOn the roster, but cannot sign in. Use Send invite.

Adding a teammate​

Add a teammate takes a name, work email, role, and access type, creates the people row, and mails an invitation. The invitation lands on auth-callback.html, which shows the set-a-password form before forwarding to the workspace.

Access type is a deliberate choice, not a guess from the email domain:

  • Employee — appears in staffing, availability, and account assignment (is_employee = true).
  • External collaborator — can sign in and is audited, but is never offered as a delivery owner.

Someone already on the roster is not added twice; the form says so and points at the Send invite button on their row, which is also how a previously revoked login is restored.

Customers are not added here​

Customer access is granted from Portal → the customer → Invite customer, which creates the person, the client_memberships row, and the invitation together. The Users screen refuses the client role for exactly this reason: a client-role person with no membership can sign in and see nothing. See the customer portal.

Roles​

admin, lead, user (the consultant value in person_role), or client. Role determines what a person can see and do; project data stays scoped to their access regardless of role — a higher role doesn't override row-level security, it changes which rows the policies let through.

Guardrails​

Only administrators reach this screen. The target role must be one of the recognized values, and the last remaining administrator cannot be demoted — a role change here can never lock the org out of its own admin surface. Invitations are rate-limited per admin, a failed invitation rolls back the person row it created, and every invite, resend, and role change writes an audit row.

Password resets for an already-active person are self-serve on the sign-in page ("Forgot password" or a magic link), so this screen does not duplicate them.

Where the code lives​