ServicesAuth

Organizations

An organization groups users under shared ownership, gives them roles, and - the part that surprises people - becomes a tenant of its own. Creating one registers a second tenancy alongside your personal one, and switching into it changes which data your requests can see at all.

Creating an organization requires a verified email on the account record. A locally minted token will not do; the check reads the stored user, so an unverified account gets 403 email_not_verified no matter what the token claims.

Create

snug auth org check-slug --slug my-org
snug auth org create --name "My Org" --slug my-org
snug auth org list
snug auth org get --id <org_id>
snug auth org update --id <org_id> --name "New Name"

Mind the flag names: get, update, and delete take --id, while invite, members, update-role, remove-member, invitations, and set-active take --org-id.

{
  "org_id": "dqxPXsVANGJNbqchQTgS",
  "name": "Docs4 Auth Org",
  "slug": "docs4-auth-org",
  "created_by": "xhMLHYzgKQPSnTSsfcmz",
  "member_count": 1,
  "sso_enforced": false,
  "created_at": "2026-08-28T22:55:46.249410Z",
  "updated_at": "2026-08-28T22:55:46.249410Z"
}

Slugs are globally unique. check-slug answers {"available": true|false}, and racing past it lands on 409 org_slug_taken. The creator becomes owner and counts as the first member.

Roles

Three roles, strictly ordered: owner, admin, member. Anything that changes the organization needs at least admin, and a member attempting it gets a precise refusal:

{
  "status": 403,
  "msg": "insufficient organization role: required admin, actual member",
  "error": "insufficient_org_role"
}

The role lives in the org_role claim of a token issued for that organization, and it is unrelated to platform_role. An organization owner has no admin powers outside their own organization.

Members and invitations

snug auth org invite --org-id <org_id> --email dev@example.com --role member
snug auth org invitations --org-id <org_id>   # sent by this org
snug auth org invitations                     # addressed to you
snug auth org accept-invite --invitation-id <id>
snug auth org decline-invite --invitation-id <id>
snug auth org members --org-id <org_id>
snug auth org update-role --org-id <org_id> --member-id <member_id> --role admin
snug auth org remove-member --org-id <org_id> --member-id <member_id>

An invitation is addressed to an email and expires in 48 hours:

{
  "invitation_id": "BQdLExeBEERfZNAKxBVy",
  "org_id": "dqxPXsVANGJNbqchQTgS",
  "email": "dev@example.com",
  "role": "member",
  "status": "pending",
  "expires_at": "2026-08-30T22:56:03.739383Z"
}

The invited user does not need the emailed link. Once they have an account with that address, snug auth org invitations lists the invitation for them by email match, which is the practical path on any deployment where mail is not actually being delivered.

Two rules are enforced on redemption. Only the addressed account may accept, so a different signed-in user gets 403 invitation_email_mismatch. And an invitation is spent once answered: declining returns 204, after which accepting the same invitation is a 409 (the message reads "already been accepted" even for a declined one, while the organization's own invitation list correctly shows "status": "declined"). Send a fresh invitation rather than trying to revive a spent one.

member_id is not a user ID. It is the composite {org_id}:{user_id} returned by members and by accept-invite:

{
  "member_id": "dqxPXsVANGJNbqchQTgS:ELjVQhdfXmswqAFSGCBR",
  "user_id": "ELjVQhdfXmswqAFSGCBR",
  "role": "member",
  "created_at": "2026-08-28T22:56:21.448381Z"
}

Role changes and removals both take that composite. remove-member answers 204 with no body, after which the member is gone from members.

Switching context, and what it costs

snug auth org set-active --org-id <org_id>
snug auth org clear-active

set-active re-issues your access token. The new one carries azp set to the organization ID and picks up an org_role claim:

Subject (sub): xhMLHYzgKQPSnTSsfcmz
Tenant ID (azp): dqxPXsVANGJNbqchQTgS
Platform role: user
Org role: owner

Because azp is the tenant boundary, this is not a cosmetic switch. Data you wrote in your personal context is simply not there in the organization context, and the other way around. Reproduced end to end:

snug auth org clear-active
snug kv set docs/probe '"personal"'          # written under the personal tenant
snug auth org set-active --org-id <org_id>
snug kv get docs/probe
{ "status": 404, "msg": "Key not found: docs/probe", "error": "key_not_found" }

Nothing was deleted; the key belongs to a tenant you are no longer acting as. clear-active returns you to the personal context and the key is readable again. Treat an organization as a separate workspace, and decide deliberately which context a piece of data belongs to before writing it.

Two practical consequences:

  • A worker that should operate on organization data needs a token minted for the organization tenant, not a personal token with an organization membership.
  • Anything that re-issues a token from scratch, such as signing in again, drops you back to the personal context until you set-active once more.

Deleting an organization

snug auth org delete --id <org_id>

Deletion removes the organization and its memberships. It does not delete the member accounts themselves.

Limits

A user may create 10 organizations and an organization may hold 100 members by default (AUTH_SERVICE_MAX_ORGS_PER_USER, AUTH_SERVICE_MAX_MEMBERS_PER_ORG), and invitations expire after 48 hours (AUTH_SERVICE_INVITATION_TTL_SECONDS). All three are in the auth CONFIG reference.

Reference

On this page