Organizations and Members - ThinkHumanDotDev/marmot GitHub Wiki
Everything in Marmot lives inside an organization: monitors, notification channels, status pages,
maintenance windows and API keys. A user can belong to several organizations with a different role in each
and switches between them from the sidebar; each organization has its own URL prefix (/acme/monitors).
The model follows kan.bn's workspaces.
Each membership has one of four roles. Permissions are resource:action strings mapped to the minimum
role that may perform them (src/access/permissions.ts); a role satisfies a permission when it ranks at or
above that minimum (owner > admin > member > viewer).
| What | viewer | member | admin | owner |
|---|---|---|---|---|
| See monitors, status pages, maintenance, members | ✓ | ✓ | ✓ | ✓ |
| Create, edit, pause, delete monitors | ✓ | ✓ | ✓ | |
| Create, edit, publish, delete status pages and incidents | ✓ | ✓ | ✓ | |
| Schedule and edit maintenance windows | ✓ | ✓ | ✓ | |
| See notification channels (secrets masked) | ✓ | ✓ | ✓ | |
| Create, edit, test, delete notification channels | ✓ | ✓ | ||
| Invite members, change roles, remove members | ✓ | ✓ | ||
| Edit organization name, slug, logo, settings | ✓ | ✓ | ||
| Manage API keys | ✓ | ✓ | ||
| Transfer ownership, delete the organization | ✓ |
Nobody can grant a role above their own (canManageRole), an admin cannot remove or demote an owner, and an
organization always keeps at least one owner: the last owner cannot be demoted, removed or leave.
The exact permission strings, for anyone writing code or reading the access functions:
| Permission | Minimum role |
|---|---|
organization:read, member:read, monitor:read, monitor-incident:read, declared-incident:read, status-page:read, maintenance:read
|
viewer |
monitor:create|update|delete, monitor-incident:acknowledge|resolve, declared-incident:create|update, status-page:create|update|delete, maintenance:create|update|delete, notification:read, subscriber:read|manage
|
member |
organization:update, member:invite|remove|update-role, notification:create|update|delete, api-key:read|create|delete, sso:read, subscriber:send, declared-incident:delete|skip-postmortem
|
admin |
organization:delete, sso:manage
|
owner |
The account created by the setup wizard is the instance superadmin. Superadmins bypass every
organization check, can open the Payload admin panel at /admin (nobody else can) and manage instance
settings there. Promote another user by ticking superadmin on their user document in the admin panel.
Keep the number small; superadmin is an operator role, not a team role.
The setup wizard creates the first organization. Afterwards any signed-in user can create more from the
organization switcher (New organization, /onboarding) and becomes its owner. Slugs are lower-case,
unique and cannot be a reserved word (admin, api, status, …); GET /api/orgs/slug-available?slug=
is what the form uses to check.
Settings → Organization (admins and owners) edits the name, slug, logo, timezone and first day of the week. The danger zone at the bottom lets an owner transfer ownership or delete the organization, which removes its invitations and memberships (delete or move its monitors, channels and status pages first).
Two mechanisms, both on the Members page (/{org}/members):
Email invitations (member:invite, i.e. admins and owners) carry an email address and a role. The
invitee receives a link to /invite/<token> that is valid for seven days; opening it while signed in joins
the organization with that role, otherwise it offers to sign in or create an account first (even when
public sign-up is disabled). Pending invitations are listed with Resend (also extends the expiry) and
Revoke. An existing member who is invited again keeps their current role.
Invite link: one shareable link per organization with a fixed role (/invite/<code>). Anyone with the
link can join, so use it for onboarding a team quickly and reset or disable it afterwards from the
Members page. Only admins and owners can see the link.
Without an SMTP server configured, invitation emails are printed to the web process log, where the link can be copied.
With SSO configured and DISABLE_SIGNUP on, a user who signs in through the identity provider for the first
time while holding a pending invitation is provisioned and placed in the organization in one step
(Single sign-on).
The members table shows every member with their role, owners first. Admins and owners can change roles (up to their own rank) and remove members (of equal or lower rank); any member can leave an organization they are not the sole owner of.
Transfer ownership (owners only, in the organization danger zone) makes another member the owner and demotes the previous owner to admin. Use it before leaving or deleting your account: an account that is the sole owner of any organization cannot be deleted until the organization has another owner.
Settings → Account (/{org}/settings/account) edits your name and avatar, your theme and language
(users.language; the picker appears once Marmot ships more than one language), changes your password (the
current password is required), manages the single sign-on identities linked to your account (Connected
accounts: link GitHub, Google or the company IdP, unlink any but the last way in; see
Single sign-on) and deletes your account (type your email to confirm;
refused while you are the sole owner of an organization). Password reset by email is available from the
login page (/forgot-password) when SMTP is configured.
Instances open to the public can require new accounts to confirm their address
(REQUIRE_EMAIL_VERIFICATION=true, or Require email verification under Settings → Instance; off by
default). While it is on, an account created on /signup starts unverified and receives a link
(/verify-email?token=…) that is valid for 24 hours and works once; opening it shows a Confirm button,
so mail scanners that follow links cannot use it up. Until then the person can sign in but only sees a
"check your inbox" screen, where they can request a new link (five per quarter hour), correct a mistyped
address (a new link goes to the new address) or sign out. Creating organizations, invitations and
notification channels answers 403 with an explanation, also through the API.
Nobody else has to do anything:
- accounts that existed before the setting was turned on, the setup wizard's administrator, superadmins and accounts created by server code count as verified;
- accepting an email invitation sent to the account's own address confirms it (an invite link does not);
- single sign-on accounts whose identity provider asserts the address (
email_verified, or an organization's own connection) are verified, and such a login also confirms a pending account with the same address.
A signed-in user who changes their address has to confirm the new one while the setting is on. Turning the
setting off lifts every restriction at once. Without SMTP_HOST the links are only written to the web
process log; the setup screen and the Instance settings page warn about it. Sent links and confirmations
are recorded in the audit log (auth.email_verification_sent, auth.email_verified with the method:
link, invitation, sso or magic-link).
People can sign in without a password when Sign-in links is on under Settings → Instance
(magicLinkEnabled, default MAGIC_LINK_ENABLED, off). The login page then offers Email me a sign-in
link instead: the person enters their address and gets a link (/login/magic-link?token=…) that is valid
for 15 minutes and works once; requesting another one revokes the previous link. Opening it shows a
Continue button, so mail scanners that follow links cannot use it up.
- No account enumeration. The answer ("if this address can sign in here, a link is on its way") is the same whether or not an account exists, and the lookup and the email happen after the response.
-
New accounts. An address without an account gets a link that creates one when sign-up is allowed
(
allowSignup/DISABLE_SIGNUP); while sign-up is off, only an address with a pending invitation does, and the invitation is accepted with it, as for single sign-on. Such accounts have no password (they can set one through Forgot password). - Two-factor authentication still applies: after the link, an account with 2FA enters its code (or a backup code) like after a password.
- Email verification: using a link confirms the address (#177).
-
Policy: a sign-in link counts as a local login, like a password. The SSO-only mode
(
OIDC_DISABLE_LOCAL_LOGIN) turns links off, break-glass included, and an organization that requires single sign-on refuses them for its verified domains exactly as it refuses passwords (owners keep their break-glass path, audited asauth.break_glass). -
Rate limits: five links per address per quarter hour (like verification and reset mail) and twenty per
client IP behind a trusted proxy (
trustProxy; without it, 100 per quarter hour for the whole instance). -
Audit log:
auth.magic_link_sentfor every link mailed,auth.loginwithmethod: magic-linkfor every sign-in,auth.login_failedfor refused links andauth.rate_limited.
Tokens are stored in Redis as SHA-256 digests only. Without SMTP_HOST the links are only written to the
web process log, so configure SMTP before turning the setting on.
| Method & path | Permission / rule |
|---|---|
POST /api/invitations { organization, email, role }
|
member:invite (Payload REST; the hook mints the token and sends the mail) |
PATCH /api/invitations/:id { status: "revoked" }
|
member:invite |
PATCH /api/orgs/:orgId/members/:userId { role }
|
member:update-role; target and new role at or below your own |
DELETE /api/orgs/:orgId/members/:userId |
own id: leave; otherwise member:remove
|
POST /api/orgs/:orgId/invitations/:invitationId/resend |
member:invite |
GET / POST / DELETE /api/orgs/:orgId/invite-link
|
member:invite: read, (re)generate, disable |
POST /api/orgs/:orgId/transfer-ownership { userId }
|
owner |
POST/DELETE /api/orgs/:orgId/logo (multipart file) |
organization:update; upload and set, or clear, the organization logo (sessions only, not API keys) |
POST /api/invite/:code/accept |
signed-in user; accepts an invitation or invite-link code |
GET / PATCH /api/account, POST /api/account/password
|
the signed-in user |
POST/DELETE /api/account/avatar (multipart file) |
the signed-in user; upload and set, or clear, your avatar |
GET /api/account/accounts, DELETE /api/account/accounts/:id
|
the signed-in user; unlinking the last identity of a password-less account is refused |
GET /api/orgs/:orgId/sso/connections, GET /api/orgs/:orgId/sso/domains
|
sso:read (admins and owners) |
POST/PATCH/DELETE under /api/orgs/:orgId/sso/*, POST …/domains/:id/verify, POST …/sso/metadata, PATCH …/sso/enforcement
|
sso:manage (owners); see Single sign-on
|
POST /api/auth/verify-email { token }
|
anyone holding the link; confirms the address it was sent to (400 when invalid, expired, used or outdated) |
POST /api/auth/verify-email/resend |
the signed-in user; mails a new link (rate limited, 429) |
DELETE /api/account { confirm: email }
|
the signed-in user, unless sole owner somewhere |
GET /api/orgs/slug-available?slug= |
signed-in user |
Memberships themselves live on users.organizations and are writable only by superadmins through the
Payload API; the routes above are the supported way to change them. Implementation notes are in
Architecture.