Skip to main content

Overview

Openlane Console supports multi-organization architecture, allowing users to belong to multiple organizations and switch between them seamlessly. Each organization has its own isolated compliance data, team members, and settings.
Organizations are the primary isolation boundary in the Console. All compliance data (controls, policies, risks, evidence) is scoped to an organization.

Organization Types

The Console supports two types of organizations:

Personal Organizations

Every user automatically gets a personal organization:
  • Purpose: Individual user workspace
  • Ownership: User is the sole owner
  • Usage: Testing, personal projects, individual compliance needs
  • Visibility: Hidden from organization switcher (marked with personalOrg: true)

Team Organizations

Organizations created for teams and companies:
  • Purpose: Multi-user collaboration
  • Members: Multiple users with different roles
  • Usage: Enterprise compliance management
  • Visibility: Shown in organization switcher

Organization Selection

Users can view and select organizations at /organization:
// apps/console/src/app/(protected)/organization/page.tsx
const OrganizationLanding: React.FC = () => {
  return (
    <section>
      <ExistingOrganizations />
      <CreateOrganizationForm />
    </section>
  )
}

Existing Organizations Component

Displays all organizations the user belongs to:
const { data } = useGetAllOrganizationsWithMembers({ 
  userID: sessionData?.user.userId 
})

// Filter out personal orgs
const orgs = data?.organizations.edges?.filter(
  (org) => !org?.node?.personalOrg
) || []
Each organization card shows:
  • Organization avatar/logo
  • Organization display name
  • User’s role in the organization
  • Action button (Select or Leave)

Organization Switching

Switching between organizations involves updating the session and redirecting:
1

Initiate Switch

const handleOrganizationSwitch = async (orgId?: string) => {
  const response = await switchOrganization({
    target_organization_id: orgId,
  })
  
  // Check if SSO redirect is needed
  if (handleSSORedirect(response)) {
    return
  }
}
2

Update Session

if (sessionData && response) {
  await updateSession({
    ...response.session,
    user: {
      ...sessionData.user,
      accessToken: response.access_token,
      organization: orgId,
      refreshToken: response.refresh_token,
    },
  })
}
New access and refresh tokens are issued for the target organization.
3

Clear Cache

requestAnimationFrame(() => {
  queryClient?.clear()
})
Clear all cached queries to prevent data leakage between organizations.
4

Redirect to Dashboard

push('/dashboard')
Navigate to the dashboard of the new organization.
Critical: Always clear the query cache when switching organizations to prevent showing data from the previous organization. Data isolation is essential for security.

Creating Organizations

Users can create new organizations:
const CreateOrganizationForm = () => {
  const handleCreate = async (data: CreateOrgInput) => {
    const newOrg = await createOrganization({
      name: data.name,
      displayName: data.displayName,
      description: data.description,
    })
    
    // Automatically switch to new org
    await handleOrganizationSwitch(newOrg.id)
  }
}
  • Name: URL-friendly identifier (e.g., acme-corp)
  • Display Name: Human-readable name (e.g., “Acme Corporation”)
  • Description: Optional description of the organization
  • Avatar: Optional logo/avatar image

Organization Settings

Access organization settings at /organization-settings:
Configure basic organization information:
  • Organization name and display name
  • Description
  • Avatar/logo upload
  • Contact information
  • Organization timezone

User Management

Manage team members at /user-management:

Inviting Users

1

Send Invitation

const inviteUser = async (email: string, role: OrgMembershipRole) => {
  await createInvite({
    email,
    role,
    organizationId: currentOrgId,
  })
}
2

User Receives Email

Invited user receives email with invitation link:
https://console.openlane.io/invite?token=inv_abc123
3

Accept Invitation

User clicks link, creates account (if needed), and joins organization.

Member Roles

Full Access:
  • All admin permissions
  • Delete organization
  • Transfer ownership
  • Cannot be removed (must transfer ownership first)
role.toUpperCase() === OrgMembershipRole.OWNER
Administrative Access:
  • Manage members (except owner)
  • Configure organization settings
  • Manage compliance data
  • Cannot delete organization
Standard Access:
  • Create and edit compliance data
  • Upload evidence
  • Complete assignments
  • View organization data
Read-Only Access:
  • View compliance data
  • Generate reports
  • No editing permissions
  • Cannot access settings

Leaving an Organization

Non-owner members can leave organizations:
const handleLeaveOrganization = async (membershipId: string, orgId: string) => {
  await leaveOrganization({ deleteOrgMembershipId: membershipId })
  
  // Switch to another organization
  const remainingOrgs = data?.organizations.edges?.filter(
    (org) => org?.node?.id !== orgId
  ) || []
  
  const nextOrg = remainingOrgs[0]?.node?.id
  if (nextOrg) {
    await handleOrganizationSwitch(nextOrg)
  }
}
Organization owners cannot leave their organization. They must first transfer ownership to another member or delete the organization.

SSO Integration with Organizations

When SSO is configured, organization switching may trigger SSO re-authentication:
const handleSSORedirect = (response: SwitchOrgResponse) => {
  if (response?.requiresSSO) {
    // Redirect to SSO provider
    window.location.href = response.ssoRedirectUrl
    return true
  }
  return false
}
This ensures users authenticate with the correct identity provider for each organization.

Query Invalidation Strategy

When performing organization-related actions, invalidate relevant queries:
// After leaving organization
queryClient.invalidateQueries({
  predicate: (query) => 
    ['memberships', 'organizationsWithMembers', 'groups']
      .includes(query.queryKey[0] as string),
})

// After switching organization
queryClient.clear()  // Clear all queries

// After updating settings
queryClient.invalidateQueries({
  predicate: (query) => {
    const [firstKey] = query.queryKey
    return firstKey === 'organizationSetting'
  },
})

Organization Context

Access current organization throughout the app:
const { currentOrgId, currentOrg } = useOrganization()

// Use in queries
const { data } = useGetOrganizationSetting(currentOrgId || '')

// Conditional rendering
if (!currentOrgId) {
  return <SelectOrganizationPrompt />
}

Best Practices

  • Always scope queries by organization ID
  • Clear cache when switching organizations
  • Validate organization membership server-side
  • Use row-level security in database
  • Cache organization list with reasonable TTL
  • Prefetch common queries on organization switch
  • Use optimistic updates for settings changes
  • Implement pagination for large member lists
  • Verify organization membership on every request
  • Enforce role-based access control
  • Audit organization switches and membership changes
  • Require re-authentication for sensitive operations
  • Show organization context in navigation
  • Confirm before leaving organization
  • Handle edge cases (last organization, pending invites)
  • Provide clear feedback during organization switch

Next Steps

Compliance Features

Manage controls, policies, and compliance data

Authentication

Configure SSO and authentication settings

User Management

Invite and manage team members

Deployment

Deploy multi-tenant architecture

Build docs developers (and LLMs) love