Aller au contenu principal

ADR-030 — Organization and User Lifecycle

1. Context

The console onboarding wizard mixed three concerns that belong to different actors and lifecycles:

  1. Organization setup (once per tenant): name, optional demo seed, SSO/domain policy — admin/owner only.
  2. User join (per person): invitation accept or SSO JIT → tenant_memberships.
  3. User activation (per person/device): browser extension install/enroll, first real detection.

Additionally, the invitation flow was incomplete: emails linked to /invite/{token} but the console had no route and the API had no preview/accept endpoints. Invited users on mature tenants were incorrectly sent through the full org onboarding wizard because gating used tenant-level onboarding_completed without role checks.

Noxys ships in three deployment topologies (see ADR-010, ADR-019):

TopologyProvisioningTypical authExtension delivery
SaaS sharedSelf-serve signup → new tenantEmail magic link + optional SSOSelf-serve store links
Dedicated tenantSales/onboarding creates tenantSSO enforced, invite-firstMDM or self-serve
Self-hosted / air-gapCustomer install + bootstrap adminSSO or local IdP, no public signupMDM / offline package

The lifecycle model must be deployment-agnostic: same states, routes, and API contracts; only defaults and policy knobs differ per topology.

2. Decision

2.1 Three-layer lifecycle (canonical)

Layer 1 — Org setup (tenant, once): tenants.onboarding_completed; actor owner/admin; UI /org-setup (2 steps: welcome+extension, optional demo).

Layer 2 — User join (per user): POST /invitations/accept or SSO JIT; data in tenant_memberships + users row in target tenant.

Layer 3 — User activation (per user/device): UI /welcome (extension install); metrics ext_installed, first_detection (ADR-022 funnel).

Rule: Layer 1 gates only admin/owner roles. Members/viewers never see org setup; they land on /welcome until they install the extension, then /dashboard.

2.2 Invitation flow (repair)

  1. Admin: POST /api/v1/users/invite → email with {console}/invite/{token}
  2. Invitee: GET /api/v1/invitations/preview?token= (public)
  3. Invitee: Login or signup (email must match invitation)
  4. Invitee: POST /api/v1/invitations/accept → membership + JWT for invited tenant
  5. Invitee: /welcome → extension → dashboard

2.3 Deployment matrix — same lifecycle, different defaults

ConcernSaaS sharedDedicated tenantSelf-hosted
Tenant creationSignupProvisioning runbookBootstrap installer
Org setup UI/org-setup for first adminSameSame
Public signupEnabledDisabled or domain-gatedDisabled
Invitation acceptSame API + consoleSameSame
/welcomeStore linksStore + MDMOffline/MDM package
onboarding_completedAdmin finishes org setupOften pre-set trueOften pre-set true
Demo seedOptionalOptionalOften skipped

Conclusion: One product lifecycle; deployment mode changes who creates the tenant, whether signup is public, and how the extension is delivered — not separate wizards or duplicate APIs.

2.4 Console routes

RouteAccessPurpose
/invite/:tokenPublic → auth → acceptInvitation landing
/org-setupAdmin/owner, !onboarding_completedOrg setup wizard
/welcomeAuthenticated non-admin OR post-inviteExtension install
/dashboardAuthenticated, tenant onboardedMain app

/onboarding remains an alias to /org-setup for backward compatibility.

2.5 Removed from org setup

Step Connect an AI provider is removed — it did not persist configuration.

3. Consequences

Positive: invited users skip org setup on mature tenants; single model for all deployment topologies.

Follow-up (Phase 3): block orphan tenant on signup when pending invitation exists; SSO tenant_memberships sync; multi-tenant JWT switching.

4. References

  • Migration 179_tenant_onboarding_completed.up.sql
  • noxys-api/internal/database/auth.go

Phase 3 (implemented 2026-06-04)

  • Signup anti-orphan: VerifyCode joins pending invitation tenant instead of CreateTenantWithOwner.
  • SSO UpsertSSOUser syncs tenant_memberships.
  • Invite guard: reject if email already has active tenant or is already a member of inviting tenant.
  • Tenant switch: GET /api/v1/auth/tenants, POST /api/v1/auth/switch-tenant.