Skip to content

User Management

Foundation owners can invite team members, define custom roles with granular permissions, and manage the team roster. Miranda supports both a legacy role enum and a new foundation-scoped Role model with fine-grained permission arrays.

The team page (GET /api/team) returns two lists:

Active team members with their assigned roles:

{
"members": [
{
"id": "uuid",
"name": "Kathy Hoff",
"email": "[email protected]",
"role_name": "Owner",
"is_owner": true,
"created_at": "2026-03-15T00:00:00Z"
}
]
}

Outstanding invitations that have not been accepted:

{
"invites": [
{
"id": "uuid",
"email": "[email protected]",
"role": { "id": "uuid", "name": "Program Director" },
"expires_at": "2026-03-22T00:00:00Z"
}
]
}

Invitations expire after 7 days.

POST /api/team/invite
{
"email": "[email protected]",
"role_id": "uuid-of-role"
}

The invite process:

  1. Validation — Checks the role belongs to the foundation, the email is not already a member, and no pending invite exists
  2. Supabase account — Creates a Supabase auth account for the invitee (or updates their password if one exists)
  3. Passphrase generation — Creates a human-readable temporary passphrase (three random words)
  4. Email — Sends a TeamInviteMail with the join link and temporary credentials
  5. Invite record — Creates a TeamInvite with a 64-character random token, expiring in 7 days

Invitees can accept via two endpoints:

  • POST /api/team/invites/{token}/accept — Creates a User record; requires the invitee to authenticate separately
  • POST /api/team/invites/{token}/accept-and-login — Creates a User record and returns Supabase session tokens for immediate login

Both endpoints create a User with the role specified in the invitation.

GET /api/team/roles

If no roles exist for the foundation, this endpoint automatically seeds default roles based on the foundation’s org_type:

  • Owner role (always created first, slug owner)
  • Additional roles from config/org-types.php with preset permissions
POST /api/team/roles
{
"name": "Program Director",
"permissions": [
"contacts.view",
"contacts.create",
"scholarships.view",
"scholarships.create",
"scholarships.review"
]
}

The slug is auto-generated from the name with a random suffix for uniqueness.

PUT /api/team/roles/{roleId}
{
"name": "Program Director",
"permissions": ["contacts.view", "contacts.create", "scholarships.view", "scholarships.create", "scholarships.review", "donations.view"]
}

The owner role cannot be modified.

DELETE /api/team/roles/{roleId}

When a role is deleted, all users assigned to that role have their role_id set to null. The owner role cannot be deleted.

Permissions are grouped by feature area. See Roles & Permissions for the complete list.

GroupPermissions
Peoplecontacts.view, contacts.create, contacts.edit, contacts.delete
Donationsdonations.view, donations.create, donation_pages.view, donation_pages.create, donation_pages.edit
Communicationscampaigns.view, campaigns.create, campaigns.edit, campaigns.send, sequences.view, sequences.create, sequences.edit
Programsscholarships.view, scholarships.create, scholarships.review
Toolsimport.use
Administrationsettings.view, settings.edit, billing.manage, team.view, team.invite, team.remove
PUT /api/team/members/{userId}/role
{
"role_id": "uuid-of-new-role"
}

Cannot change the role of a user with the owner legacy role.

DELETE /api/team/members/{userId}

Guardrails:

  • Cannot remove yourself
  • Cannot remove a user with the owner legacy role
  • Deletes the User record (the Supabase auth account is not affected)
GET /api/team/permissions

Returns the full permission structure from config/permissions.php:

{
"groups": {
"people": {
"label": "People",
"permissions": {
"contacts.view": "View contacts",
"contacts.create": "Create contacts",
"contacts.edit": "Edit contacts",
"contacts.delete": "Delete contacts"
}
}
}
}

This powers the role editor UI, where admins can toggle individual permissions for each custom role.