UTA DevHub

Multi-Tenant Headers

Detailed implementation of multi-tenant headers for organization and project context

Multi-Tenant Headers

Overview

This guide provides a detailed implementation for multi-tenant headers using the architecture defined in the Header Customization Architecture document. Multi-tenant headers enable your API to understand the organizational context of each request, allowing proper data segmentation and access control in multi-tenant applications.

Quick Start

If you need to quickly implement tenant headers in your application, follow these three steps:

  1. Create your tenant store:

    // core/domains/tenant/store.ts
    export const useTenantStore = create<TenantState>()(
      persist(
        (set) => ({
          organizationId: null,
          projectId: null,
          setOrganization: (id) => set({ organizationId: id, projectId: null }),
          setProject: (id) => set({ projectId: id }),
        }),
        { name: 'tenant-storage' }
      )
    );
  2. Implement tenant header provider:

    // core/domains/tenant/headerProvider.ts
    export class TenantHeaderProvider implements HeaderProvider {
      id = 'tenant';
      priority = 100;
      
      async getHeaders() {
        const { organizationId, projectId } = useTenantStore.getState();
        return {
          [API_HEADERS.ORGANIZATION_ID]: organizationId || undefined,
          [API_HEADERS.PROJECT_ID]: projectId || undefined,
        };
      }
    }
  3. Register with your API client:

    // core/shared/api/client.ts
    const apiClient = new AuthenticatedApiClient(config);
    apiClient.registerHeaderProvider(new TenantHeaderProvider());

For complete implementation details, continue reading the sections below.

Purpose & Scope

This implementation guide covers:

  • Zustand store for tenant context management
  • Header provider implementation for tenant headers
  • UI components for tenant selection
  • Testing strategies specific to tenant headers
  • Backend integration considerations

Prerequisites

Before implementing multi-tenant headers, ensure you're familiar with:

Tenant Header Architecture

The multi-tenant header system consists of three main components:

Tenant Store Implementation

The tenant store manages the current organization and project selection:

// core/domains/tenant/store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
 
export interface TenantState {
  // State
  organizationId: string | null;
  projectId: string | null;
  organizationName: string | null;
  projectName: string | null;
  
  // Actions
  setOrganization: (id: string | null, name: string | null) => void;
  setProject: (id: string | null, name: string | null) => void;
  clearTenant: () => void;
}
 
export const useTenantStore = create<TenantState>()(
  persist(
    (set) => ({
      // Initial state
      organizationId: null,
      projectId: null,
      organizationName: null,
      projectName: null,
      
      // Actions
      setOrganization: (id, name) => set({
        organizationId: id,
        organizationName: name,
        // Clear project when organization changes
        projectId: null,
        projectName: null,
      }),
      
      setProject: (id, name) => set({
        projectId: id,
        projectName: name,
      }),
      
      clearTenant: () => set({
        organizationId: null,
        projectId: null,
        organizationName: null,
        projectName: null,
      }),
    }),
    {
      name: 'tenant-storage',
      // Only persist IDs, not names (they can be refetched)
      partialize: (state) => ({
        organizationId: state.organizationId,
        projectId: state.projectId,
      }),
    }
  )
);

Tenant Header Provider

The header provider reads from the tenant store and adds the appropriate headers:

// core/domains/tenant/headerProvider.ts
import { HeaderProvider } from '@/core/shared/api/headers/types';
import { API_HEADERS } from '@/core/shared/constants/headers';
import { useTenantStore } from './store';
 
export class TenantHeaderProvider implements HeaderProvider {
  id = 'tenant';
  priority = 100; // High priority
  
  async getHeaders(): Promise<Record<string, string | undefined>> {
    // Get current state values using Zustand's getState()
    const { organizationId, projectId } = useTenantStore.getState();
    
    return {
      [API_HEADERS.ORGANIZATION_ID]: organizationId || undefined,
      [API_HEADERS.PROJECT_ID]: projectId || undefined,
    };
  }
}

UI Implementation

Tenant Selector Component

A reusable component for selecting the current organization and project:

// features/tenant/components/TenantSelector.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useTenantStore } from '@/core/domains/tenant/store';
import { useOrganizations, useProjects } from '@/core/domains/organizations/hooks';
import { Dropdown } from '@/ui/Dropdown';
 
export const TenantSelector: React.FC = () => {
  const { 
    organizationId, 
    projectId, 
    setOrganization, 
    setProject 
  } = useTenantStore();
  
  // Fetch available organizations
  const { data: organizations, isLoading: orgsLoading } = useOrganizations();
  
  // Fetch projects for selected organization
  const { 
    data: projects, 
    isLoading: projectsLoading 
  } = useProjects(organizationId, { enabled: !!organizationId });
  
  const handleOrgChange = (id: string) => {
    const org = organizations?.find(o => o.id === id);
    setOrganization(id, org?.name || null);
  };
  
  const handleProjectChange = (id: string) => {
    const project = projects?.find(p => p.id === id);
    setProject(id, project?.name || null);
  };
  
  return (
    <View style={styles.container}>
      <View style={styles.selectorContainer}>
        <Text style={styles.label}>Organization</Text>
        <Dropdown
          placeholder="Select organization"
          value={organizationId}
          items={organizations?.map(org => ({ 
            label: org.name, 
            value: org.id 
          })) || []}
          onValueChange={handleOrgChange}
          disabled={orgsLoading}
          loading={orgsLoading}
        />
      </View>
      
      {organizationId && (
        <View style={styles.selectorContainer}>
          <Text style={styles.label}>Project</Text>
          <Dropdown
            placeholder="Select project"
            value={projectId}
            items={projects?.map(project => ({ 
              label: project.name, 
              value: project.id 
            })) || []}
            onValueChange={handleProjectChange}
            disabled={projectsLoading || !organizationId}
            loading={projectsLoading}
          />
        </View>
      )}
    </View>
  );
};
 
const styles = StyleSheet.create({
  container: {
    marginBottom: 16,
  },
  selectorContainer: {
    marginBottom: 12,
  },
  label: {
    fontSize: 14,
    fontWeight: '500',
    marginBottom: 4,
  },
});

Tenant Header Display

A component to show the current tenant context:

// features/tenant/components/TenantHeader.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useTenantStore } from '@/core/domains/tenant/store';
 
export const TenantHeader: React.FC = () => {
  const { organizationName, projectName } = useTenantStore();
  
  if (!organizationName) {
    return null;
  }
  
  return (
    <View style={styles.container}>
      <Text style={styles.orgText}>{organizationName}</Text>
      {projectName && (
        <>
          <Text style={styles.separator}>/</Text>
          <Text style={styles.projectText}>{projectName}</Text>
        </>
      )}
    </View>
  );
};
 
const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 8,
  },
  orgText: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  separator: {
    fontSize: 16,
    marginHorizontal: 4,
    color: '#888',
  },
  projectText: {
    fontSize: 16,
    color: '#444',
  },
});

Integration with API Clients

Register the tenant header provider with your API clients:

// core/shared/api/client.ts
import { PublicApiClient, AuthenticatedApiClient } from '@/core/shared/api/base';
import { TenantHeaderProvider } from '@/core/domains/tenant/headerProvider';
 
// Create API client instances
export const publicApi = new PublicApiClient({
  baseURL: process.env.API_BASE_URL,
});
 
export const authenticatedApi = new AuthenticatedApiClient({
  baseURL: process.env.API_BASE_URL,
});
 
// Register the tenant header provider
authenticatedApi.registerHeaderProvider(new TenantHeaderProvider());

Backend Integration

The backend API will need to extract and validate the tenant headers:

// Example Express middleware for tenant context (backend code)
const tenantMiddleware = (req, res, next) => {
  const orgId = req.headers['x-org-id'];
  const projectId = req.headers['x-project-id'];
  
  if (!orgId) {
    return res.status(401).json({ 
      error: 'Organization ID header is required' 
    });
  }
  
  // Set tenant context for this request
  req.tenantContext = {
    organizationId: orgId,
    projectId: projectId || null,
  };
  
  // Continue with request
  next();
};
 
// Use in routes
app.use('/api/protected', authMiddleware, tenantMiddleware, protectedRoutes);

Testing Tenant Headers

// __tests__/core/domains/tenant/store.test.ts
import { useTenantStore } from '@/core/domains/tenant/store';
 
describe('TenantStore', () => {
  beforeEach(() => {
    // Reset store state before each test
    useTenantStore.setState({
      organizationId: null,
      projectId: null,
      organizationName: null,
      projectName: null,
    });
  });
  
  it('should set organization', () => {
    const orgId = 'org-123';
    const orgName = 'Test Organization';
    
    useTenantStore.getState().setOrganization(orgId, orgName);
    
    expect(useTenantStore.getState().organizationId).toBe(orgId);
    expect(useTenantStore.getState().organizationName).toBe(orgName);
  });
  
  it('should clear project when organization changes', () => {
    // Set initial org and project
    useTenantStore.getState().setOrganization('org-1', 'Org 1');
    useTenantStore.getState().setProject('proj-1', 'Project 1');
    
    // Verify both are set
    expect(useTenantStore.getState().organizationId).toBe('org-1');
    expect(useTenantStore.getState().projectId).toBe('proj-1');
    
    // Change organization
    useTenantStore.getState().setOrganization('org-2', 'Org 2');
    
    // Project should be cleared
    expect(useTenantStore.getState().organizationId).toBe('org-2');
    expect(useTenantStore.getState().projectId).toBeNull();
  });
  
  it('should clear all tenant data', () => {
    // Set initial state
    useTenantStore.getState().setOrganization('org-1', 'Org 1');
    useTenantStore.getState().setProject('proj-1', 'Project 1');
    
    // Clear tenant
    useTenantStore.getState().clearTenant();
    
    // Verify all cleared
    expect(useTenantStore.getState().organizationId).toBeNull();
    expect(useTenantStore.getState().projectId).toBeNull();
    expect(useTenantStore.getState().organizationName).toBeNull();
    expect(useTenantStore.getState().projectName).toBeNull();
  });
});

Best Practices

Tenant Header Security

Organization and project IDs should be treated as sensitive identifiers. While not as sensitive as authentication tokens, they should be protected from tampering and manipulation.

Key security considerations:

  • Use non-sequential, non-predictable IDs for organizations and projects
  • Validate tenancy on the server for every authenticated request
  • Implement proper access control to prevent tenant ID spoofing
  • Consider using signed tenant tokens for high-security applications

Performance Considerations

  • Store recently used projects in memory to reduce selection operations
  • Consider batching organization/project data for offline access
  • Cache tenant validation on the server to reduce database lookups

User Experience

  • Save and restore the last used organization and project
  • Provide clear visual indication of the current tenant context
  • Consider auto-selecting projects if there's only one available
  • Allow quick switching between frequently used contexts

Tenant Header Use Cases

  1. Data Segmentation: Ensure API responses only include data from the selected organization and project.

  2. Multi-tenant Dashboards: Display metrics and data specific to the current organization context.

  3. Role-Based Access Control: Combine tenant headers with user roles for fine-grained permissions.

  4. Audit Logging: Include tenant context in all audit logs to track actions across organizations.

Troubleshooting

IssuePossible CauseSolution
Missing tenant headersTenant not selected in UIEnsure organization is selected and stored
Missing project contextProject not selectedVerify project ID is set in tenant store
401 Unauthorized errorsMissing required tenant headersCheck if server requires tenant headers for this endpoint
Incorrect data returnedWrong tenant context activeVerify the correct organization/project is selected

Migration Considerations

If you're migrating from a different tenant identification approach, follow these steps to transition smoothly:

From Context/Service-based Approach

If you previously used a context service pattern:

  1. Create the Zustand store first, maintaining the same interface where possible
  2. Initialize the store with any existing tenant data from your current implementation
  3. Keep both implementations running in parallel during transition
  4. Update components to use the Zustand store instead of the context service
  5. Remove the old implementation once all components have been updated

From URL-based Tenant Identification

If you were passing tenant information via URL segments:

  1. Implement route parameter extraction to set the tenant store state
  2. Add navigation middleware that updates the tenant store when routes change
  3. Gradually transition endpoints to use headers instead of URL parameters

From Per-request Header Addition

If you were manually adding tenant headers to each request:

  1. Create the tenant store to centralize tenant state
  2. Implement the header provider as shown in this guide
  3. Remove manual header addition from individual API calls
  4. Update tests to mock the tenant store instead of individual headers

Summary

Implementing tenant headers is critical for multi-tenant applications. By following the patterns in this guide, you can create a clean, maintainable solution that:

  1. Stores tenant context in Zustand for global access
  2. Automatically adds tenant headers to API requests
  3. Provides a consistent UI for tenant selection
  4. Ensures proper testing across all layers

This implementation aligns with our broader API header customization architecture while providing specific guidance for the multi-tenant use case.