Membership & Invitations
Users join organizations through email invitations. An admin sends an invite, the recipient signs in (or signs up), accepts, and becomes a member with the assigned role.
In the Dashboard
- Open an organization's detail page.
- Switch to the Invitations tab.
- Click Invite member, enter the recipient's email, and pick a role.
- The recipient receives a branded email with an Accept Invitation link.
- When they accept, they appear in the Members tab and the invitation is marked accepted.
Admins can revoke pending invitations from the same tab.
Invitation Flow
Admin sends invitation
POST /api/organizations/:id/invitations creates the invitation row, then sends the email. If the email send fails, the row is rolled back so the admin can retry.
Recipient receives email
The email links to {appPublicBaseUrl}/accept-org-invite?token=<64-hex-token>.
Recipient signs in
If the recipient isn't authenticated, they're sent through the normal sign-in flow with a redirect back to the accept page.
Recipient accepts
The frontend POSTs /api/organizations/accept-invitation with the token. The server verifies the token matches a pending, unexpired invitation and that the caller's email matches the invitation, then adds the membership atomically.
API
Invite a member
POST /api/organizations/:id/invitations{ "email": "newmember@example.com", "role": "member" }Admin-only. Returns 409 if there's already a pending invitation for that email or the user is already a member. Returns 502 if the invitation row was created but the email failed to send (the row is rolled back).
List invitations
GET /api/organizations/:id/invitations?status=pending&page=1&limit=20Admin-only. status may be pending, accepted, or expired. Tokens are never returned.
Revoke an invitation
DELETE /api/organizations/:id/invitations/:invitation_idAdmin-only.
Accept an invitation
POST /api/organizations/accept-invitation{ "token": "<64-hex-character token>" }Requires the caller to be authenticated. The caller's email (lower-cased) must match the invitation's email. Returns 400 for unknown/expired tokens (anti-enumeration), 403 for email mismatch, 409 if the caller is already a member.
List members
GET /api/organizations/:id/members?page=1&limit=20Any member can read. Returns { user_id, email, name, role, created_at, updated_at } per row.
Update a member's role
PUT /api/organizations/:id/members/:user_id{ "role": "admin" }Admin-only. Returns 409 (last-admin guard) if the change would leave the organization with zero admins.
Remove a member
DELETE /api/organizations/:id/members/:user_idAdmin-only. Same 409 last-admin guard applies.
Multiple Organizations
A user can belong to multiple organizations within the same application. A freelancer could be a member of several client organizations simultaneously; an active-organization session claim and switcher UI are on the roadmap.