Account Tenancy Model
Explanation of personal workspaces and team organizations, org-only accounts, and landing behavior.
Account Tenancy Model
Overview
UptimeHunt uses an org-centric tenancy model: every resource (service, project, alert rule, integration) lives inside an organization. An organization is a bounded tenant with its own members, roles, audit log, and billing plan.
This page explains:
- the two org types and when a personal workspace exists
- which accounts are org-only (no personal workspace)
- where you land after signing in
- how to create a personal workspace after the fact
- the opt-in deprovisioning setting that ties continued membership to an active IdP or GitHub-org mapping
Two Org Types
| Personal workspace | Team organization | |
|---|---|---|
| Created by | Automatic during personal sign-up | Explicitly at sign-up, by conversion, or via POST /api/v1/orgs |
| Members | You alone | Invited members with roles |
| Roles | — (you own everything) | Owner / Admin / Member |
| SSO | — | OIDC, SAML 2.0, Google Workspace, GitHub |
| Audit log | Your own activity | Organization-wide, role-gated |
| Token oversight | Your own tokens | Owners see all members' org tokens |
| Plan | Free and paid plans | Team / Enterprise plans (14-day trial) |
Personal Workspaces Are Optional
Not every UptimeHunt account has a personal workspace. Three creation paths produce accounts that belong to a team organization only, with no personal side-channel:
| Path | Result |
|---|---|
| Sign up as a team — choose Team at registration | Founder gets a team org; no personal workspace |
| Accept an organization invitation as a new user | Account created through the invitation link; no personal workspace |
| SSO JIT provisioning — first sign-in through an org's identity provider | JIT-provisioned realm account; no personal workspace |
This is deliberate: in a team setting there is no personal side-channel for resources or API tokens that would escape the organization's roles, audit log, and token oversight. Users who later want a personal workspace can create one on demand — see Creating a Personal Workspace Later.
A user who signed up personally has a personal workspace, and it behaves as a single-user account independent of any team organizations they also belong to.
Where You Land After Signing In
The app resolves landing destination in this order, using the personal_org_slug, last_active_org_slug, and default_org_slug fields from GET /api/v1/auth/me:
| Scenario | Landing behavior |
|---|---|
| Returning user (previously acted in an org) | Restores the last-active org context (last_active_org_slug) — you land where you left off |
| First login (single org) | Lands directly in that org (default_org_slug); no chooser needed |
| First login (multi-org user, no history) | Shows the org chooser — one-time until a preference is set |
| Zero-org user | Directed to org creation or the Create your personal workspace flow |
The breadcrumb (last_active_org_slug) is updated every time you make an authenticated API request under a specific org context (the X-Org-Slug header). It is cleared to null if the org is no longer one you are a member of — a removed membership or a dissolved org never pins the landing.
Org Chooser for Zero-Org Users
If a new account ends up with no org membership at all (an edge case — for example after all team memberships have been removed and no personal workspace was created), the app presents the first-org chooser: create a personal workspace (POST /api/v1/auth/personal-org) or accept a pending invitation. API clients in this state receive ORG_CONTEXT_REQUIRED on any org-scoped request until at least one org is established.
GET /auth/me Fields
GET /api/v1/auth/me exposes three org-context fields the frontend reads on boot:
| Field | Type | Meaning |
|---|---|---|
personal_org_slug | string | null | The caller's personal workspace slug, or null if they have none |
last_active_org_slug | string | null | The last org the caller acted in; null if unset or no longer a member |
default_org_slug | string | null | Convenience: personal org, else sole org, else null (multi-org with no default) |
orgs[] | array | Every membership (team orgs first alphabetically, personal org last) |
Example for a personal-only user:
{
"personal_org_slug": "u-42",
"last_active_org_slug": null,
"default_org_slug": "u-42",
"orgs": [{"slug": "u-42", "org_type": "personal", "role": "owner", ...}]
}Example for a team-only account that has visited its org:
{
"personal_org_slug": null,
"last_active_org_slug": "acme",
"default_org_slug": "acme",
"orgs": [{"slug": "acme", "org_type": "team", "role": "member", ...}]
}Creating a Personal Workspace Later
A team-only or social account can create a personal workspace at any time using the org-switcher's Create personal workspace button, or the API:
POST /api/v1/auth/personal-org
Authorization: Bearer {access_token}Response (201 Created):
{
"data": {
"id": 17,
"slug": "u-42",
"name": "My Workspace",
"org_type": "personal",
"role": "owner",
"capabilities": ["service.view", "service.edit", ...]
}
}Rules:
- At most one personal workspace per account — a second call returns
409 Conflict("You already have a personal organization"). The UI hides the button once a personal org exists. - Creating a personal workspace mints no new login method — the account is already authenticated; only the org row and its owner membership are created.
- The new personal workspace starts empty — no resources are migrated from the team org(s).
- A personal workspace created this way can later be converted to a team organization if needed.
Personal sign-up users already have a workspace
This endpoint is for accounts that were created without one (team sign-up, invitation, JIT). If you signed up personally, you already have a personal workspace — calling this returns 409.
Deprovisioning: Membership Requires Continued IdP or GitHub-Org Membership
By default, a team-org membership persists until an Owner manually removes the member. The "Membership requires continued IdP/GitHub-org membership" org setting (membership_requires_idp) changes this: the organization opts into automatic deprovisioning — a member whose IdP role-mapping or GitHub-org membership lapses is removed automatically at their next sign-in.
Default: off
membership_requires_idp is false by default. Without it, a lapsed IdP mapping or GitHub-org removal only affects the member's role (updated by role re-sync), not their membership. Enable it only when you want the IdP or GitHub-org to be the authoritative source of who belongs to the organization.
How It Works
| Trigger | Deprovisioning action |
|---|---|
OIDC / SAML / Google Workspace sign-in — role-mapping evaluation returns deny for the member | If the org has membership_requires_idp: true, the membership is revoked automatically. Without the setting, the sign-in is refused but the membership is kept. |
GitHub (platform app) sign-in — GitHub-org membership is confirmed false for the member's GitHub login | If the org has membership_requires_idp: true and github_org is set, the membership is revoked automatically. |
GitHub team-role rule evaluation returns deny | Same as above — membership is revoked if opted in. |
Deprovisioning is best-effort on transient errors: a GitHub API outage or an unreadable OIDC groups claim never auto-removes a member. The member is offboarded only when non-membership is definitively confirmed. This prevents false offboarding during provider downtime.
The sole Owner is never auto-deprovisioned — the last-owner invariant applies: a deprovisioning decision that would leave the organization without any Owner is skipped, and the Owner keeps their membership and can sign in. Promote another Owner before removing the last one.
Audit Trail
Every automatic deprovisioning emits a member.deprovisioned audit event (visible in Organization → Audit Log) with the reason (sso_role_mapping_denied, github_org_membership_lapsed, github_team_role_denied). Manually-removed members appear as member.removed regardless of this setting.
Configuring the Setting
Not yet self-service
membership_requires_idp is implemented at the backend level and enforced at sign-in,
but it is not yet exposed via the REST API or the Settings UI. The GET /api/v1/orgs/{slug}
response never includes this field, and a PATCH body containing it is silently ignored
(Django REST Framework drops unknown fields on partial update). There is also no frontend
form control for it. To enable automatic deprovisioning for your organization, contact
UptimeHunt support.
Enabling this setting is irreversible in practice
Once enabled, any member whose IdP group or GitHub-org mapping lapses at their next sign-in will be deprovisioned silently. Verify that every current member has a valid mapping before enabling, or you may lose members unexpectedly at their next login. Deprovisionings are audited but cannot be undone automatically — removed members must be re-invited.
Interaction with Enforced SSO
membership_requires_idp and enforced_sso are orthogonal:
- Enforced SSO gates the login method — members must authenticate through the org IdP (interactive sign-in only; API tokens keep working).
membership_requires_idpgates membership itself — a member whose mapping lapses is removed, not just refused a sign-in.
Both can be active at the same time. An organization with both enabled gets the strongest lifecycle guarantee: only active IdP members can sign in AND membership is revoked the moment the IdP mapping lapses.
Summary
| Feature | API field / endpoint |
|---|---|
| Check if you have a personal workspace | personal_org_slug in GET /api/v1/auth/me |
| Restore landing context | last_active_org_slug in GET /api/v1/auth/me |
| Create a personal workspace on demand | POST /api/v1/auth/personal-org |
| Opt org into automatic deprovisioning | Contact UptimeHunt support (not yet self-service via API or UI) |