UniAuth

Organizations, teams & billing

UniAuth models teams and enterprises as a single organization entity. This guide covers how an organization is created, how its tier decides which features are unlocked, the seat cap, per-organization billing, and what happens when a subscription lapses.

One entity, two tiers

There is exactly one organizations entity. Its tier classifies it as a Team or an Enterprise:

  • Team members, roles and invitations, native groups, OAuth clients, and an audit log.
  • Enterprise everything in Team, plus inbound SSO (SAML SP + OIDC federation), SCIM 2.0 provisioning, and custom-domain verification.

The tier is derived from the organization's own per-org Stripe subscription — there is no separate plan flag to keep in sync.

Scope of tier gating: Tier gating applies only to the org-admin UI and the groups claim are tier-independent and unchanged. The OAuth/OIDC/SAML protocol endpoints and the /api/org/* routes — integrating an app never depends on an org's tier.

Creating a team (self-serve)

Any signed-in user can create a team with POST /api/user/organizations — no admin involvement required. New organizations start on the Team tier.

POST /api/user/organizations
Content-Type: application/json
Authorization: Bearer <user-session>

{
  "name": "Acme Inc"
}

# Response — you become the org owner
{
  "success": true,
  "data": { "id": "org_...", "name": "Acme Inc", "tier": "team" }
}
  • You become the owner of the organization you create.
  • Each user may own at most 10 organizations; creation is atomic.
  • Joining organizations created by others is unlimited — the cap is on owned organizations only.

From the UI, use the “Create team” / “Manage” controls in your organizations in your account.

The org admin area

Owners and admins manage their organization from a delegated admin area at /org/[id]/admin/{users,groups,clients,domains,sso,billing,settings,policies,audit}. Each tab maps to org-scoped APIs.

  • users · members, roles, and invitations
  • groups · native groups and membership
  • clients · OAuth clients owned by the organization
  • domains · custom-domain verification (Enterprise)
  • sso · inbound SAML and OIDC federation (Enterprise)
  • billing · the per-org Stripe subscription and seats
  • settings · organization profile and preferences
  • policies · access and security policies
  • audit · the organization's audit events

Roles and isolation

Every /api/org/[id]/* route gates on verifyOrgRole(role) before doing any work. Three roles apply:

  • owner · full control, including billing and deletion.
  • admin · manage members, groups, clients, and enterprise settings.
  • member · belongs to the organization with no admin rights.

Org routes always FORCE the org_id from the authenticated principal — never from the request body or path. A cross-org request returns 404, so one organization can never read or write another's data.

Team vs Enterprise — what's gated

Team covers everyday collaboration. Enterprise adds the identity-federation surface. The matrix below shows where the line falls:

CapabilityTeamEnterprise
Members, roles & invitations
Native groups
OAuth clients
Audit log
Inbound SSO (SAML SP + OIDC)
SCIM 2.0 provisioning
Custom-domain verification

Calling an Enterprise-only endpoint on a Team org returns HTTP 402 with { "code": "upgrade_required" } — upgrade the organization to unlock it.

# Calling an Enterprise-only endpoint on a Team org
POST /api/org/<orgId>/saml-idps
→ 402 Payment Required
{
  "success": false,
  "code": "upgrade_required"
}

The 25-member cap

A Team-tier organization is capped at 25 members. The limit is enforced both when an invitation is sent and when it is accepted, so a pending invite can never push an org over the cap.

Exceeding the cap returns HTTP 402 with { "code": "member_limit_reached" }. Raise the limit by upgrading to Enterprise.

# 26th member, either at invite OR at accept
POST /api/org/<orgId>/invitations
→ 402 Payment Required
{
  "success": false,
  "code": "member_limit_reached"
}

Per-org billing

Billing is scoped to the organization, not the user who created it.

  • Each organization is its own Stripe customer.
  • Subscriptions are billed per seat.
  • One owner can hold separate subscriptions for multiple organizations, each invoiced independently.

Owners manage the subscription, seats, and invoices at /org/[id]/admin/billing.

What happens on downgrade

Enterprise features follow the subscription state, but a lapse never locks members out.

  • Cancellation: a genuine end of subscription ( canceled/unpaid) disables the organization's SSO identity providers and revokes its SCIM tokens.
  • Past due: a Stripe past_due status keeps the Enterprise tier during the dunning grace period — nothing is disabled yet.
  • Re-subscribing: restores the tier and automatically re-enables the identity providers that were auto-disabled on cancellation.

Members keep access. Losing the Enterprise tier only disables federation — members can always sign in by email or via Forgot Password.

Enterprise accounts

An Enterprise-tier organization can own other organizations — an organization-of-organizations, much like a GitHub Enterprise account sits above many orgs. The link is a single parent reference and nesting is one level deep.

  • Create child orgs from the enterprise — owner-only, and always a brand-new org (existing organizations are never adopted): POST /api/org/[id]/children
  • An enterprise owner or admin automatically administers every child org, so the same /org/[id]/admin tools work across all of them — without being added to each one.
  • Child orgs inherit the enterprise's features (SSO, SCIM, verified domains) and unlimited seats; there is no separate subscription per child.

Apps can also request the org OIDC scope to receive the user's role within the requesting organization (id, slug, name, role), resolved per-client just like the groups claim.