Skip to content

Multi-Tenancy

Totis uses a workspace-based multi-tenant architecture to provide isolated environments for different organizations or teams.

Architecture Overview

User
 └── Workspaces (many-to-many)
      └── Projects (one-to-many)
           └── Builds (one-to-many)
                └── Files (one-to-many)

Workspaces

Workspaces are the top-level organizational unit. They provide:

  • Isolation: Each workspace has its own projects, builds, and storage
  • Team Management: Invite members with different roles
  • Resource Limits: Storage, users, and project limits based on subscription
  • Custom Storage: Optional custom S3 storage configuration

Workspace Structure

Component Description
Name Display name for the workspace
Slug URL-safe identifier (auto-generated)
Members Users with assigned roles
Projects Applications/components
Storage S3 bucket for build artifacts

Projects

Projects represent individual applications or components within a workspace:

  • Each project has a unique slug within its workspace
  • Projects contain builds organized by version
  • Builds are isolated between projects

Builds

Builds are versioned releases of a project:

  • Identified by version and build number
  • Contain metadata (platform, type, commit info)
  • Store one or more build artifact files
  • Support public link sharing

Data Access Control

All data access is controlled by the DataAccessValidator which:

  1. Extracts user context from JWT token
  2. Validates workspace membership
  3. Checks role-based permissions
  4. Enforces tenant isolation
// Every request goes through validation
UserContext userContext = dataAccessValidator.getUserContext();
Workspace workspace = dataAccessValidator.validateAndGetWorkspaceWithPermissions(
    workspaceSlug,
    List.of(Permission.PROJECT_READ)
);

Request Flow

  1. Authentication: JWT token validated by HttpAuthenticationFilter
  2. User Context: User ID and claims stored in request scope
  3. Workspace Validation: Slug resolved and membership verified
  4. Permission Check: Role-based permissions validated
  5. Data Access: Query scoped to the validated workspace

Tenant Isolation

Database Level

All queries are scoped by workspace ID:

// Projects are always fetched within a workspace context
List<Project> projects = projectRepository.findByWorkspaceId(workspace.getWorkspaceId());

Storage Level

Build files are organized by workspace and project:

s3://bucket/
  └── {workspace-slug}/
       └── {project-slug}/
            └── {build-id}/
                 └── files...

API Level

All resource paths include workspace identification:

/api/v1/workspace/{workspaceSlug}/project/{projectSlug}/build/{buildId}

Cross-Tenant Considerations

User Membership

A user can be a member of multiple workspaces:

  • Different roles in each workspace
  • Separate permissions per workspace
  • Independent access to each workspace's resources

Workspace Switching

The JWT token is workspace-agnostic. The workspace context is determined by:

  1. The workspaceSlug path parameter in API requests
  2. Validation that the user is a member of that workspace
  3. Permission checks based on the user's role in that specific workspace

Data Boundaries

  • Users cannot access resources outside their workspaces
  • Workspace members can only see other members of the same workspace
  • Storage quotas are enforced per workspace

Best Practices

For API Consumers

  1. Always specify workspace: Include workspaceSlug in all requests
  2. Handle 403 errors: User may not have access to requested workspace
  3. Check permissions: Verify required permissions before operations

For Integrations

  1. Store workspace context: Remember which workspace the user is working in
  2. List workspaces first: Use /user/workspaces to get available workspaces
  3. Validate before operations: Check user has required permissions

Example: Multi-Workspace User

A user belonging to two workspaces sees different data in each:

# Get workspaces
curl -H "Authorization: Bearer $TOKEN" \
  https://api.usetotis.com/api/v1/user/workspaces

# Response shows both workspaces
[
  {"slug": "company-a", "role": "OWNER", "permissions": ["WORKSPACE_DELETE", ...]},
  {"slug": "client-b", "role": "DEVELOPER", "permissions": ["BUILD_CREATE", ...]}
]

# Access company-a's projects (full access)
curl -H "Authorization: Bearer $TOKEN" \
  https://api.usetotis.com/api/v1/user/workspaces/company-a/projects

# Access client-b's projects (limited access)
curl -H "Authorization: Bearer $TOKEN" \
  https://api.usetotis.com/api/v1/user/workspaces/client-b/projects