Skip to content

Component: BFF Tenant Aware

Date: December 16, 2025
Status: Architecture Specification
Component Type: Backend-for-Frontend Layer


Table of Contents

  1. Overview
  2. Architecture
  3. Tenant Context Extraction
  4. Tenant Middleware
  5. Tenant Router
  6. FastAPI Client Integration
  7. Security & Isolation
  8. Error Handling
  9. Caching Strategy
  10. Implementation Details
  11. Testing Strategy
  12. Deployment

Overview

The BFF Tenant Aware is a Node.js/TypeScript Backend-for-Frontend layer that acts as a tenant-aware proxy between the React frontend and FastAPI backend services. It extracts tenant context from incoming requests, validates tenant access, and routes requests to appropriate backend APIs with tenant context.

Key Responsibilities

  • Tenant Identification: Extract tenant ID from various sources
  • Tenant Validation: Verify tenant exists and user has access
  • Request Routing: Route requests to tenant-specific backend APIs
  • Context Injection: Add tenant context to all backend API calls
  • Response Shaping: Transform backend responses for frontend consumption
  • Error Handling: Handle tenant-related errors gracefully

Benefits

  • ✅ Centralized tenant logic
  • ✅ Type-safe API layer (tRPC)
  • ✅ Request aggregation
  • ✅ Response transformation
  • ✅ Caching layer
  • ✅ Security boundary

Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────┐
│              Frontend (React)                            │
│  - Tenant ID in request (subdomain/header/path)         │
└────────────────────┬────────────────────────────────────┘
                     │ HTTP Request
                     │ (with tenant context)
                     ▼
┌─────────────────────────────────────────────────────────┐
│              BFF Tenant Aware                            │
│                                                          │
│  ┌──────────────────────────────────────────────────┐  │
│  │  1. Tenant Context Extraction                    │  │
│  │     - Subdomain parsing                          │  │
│  │     - Header extraction                          │  │
│  │     - Path parsing                               │  │
│  │     - JWT token parsing                          │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                                │
│                        ▼                                │
│  ┌──────────────────────────────────────────────────┐  │
│  │  2. Tenant Validation                             │  │
│  │     - Verify tenant exists                        │  │
│  │     - Check user access                           │  │
│  │     - Validate tenant status                      │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                                │
│                        ▼                                │
│  ┌──────────────────────────────────────────────────┐  │
│  │  3. Tenant Context Injection                     │  │
│  │     - Add tenant ID to context                   │  │
│  │     - Add tenant config to context                │  │
│  │     - Add CAT list to context                     │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                                │
│                        ▼                                │
│  ┌──────────────────────────────────────────────────┐  │
│  │  4. Request Routing                               │  │
│  │     - Route to tenant-specific APIs               │  │
│  │     - Add tenant headers                          │  │
│  │     - Transform requests                          │  │
│  └──────────────────────────────────────────────────┘  │
│                        │                                │
│                        ▼                                │
│  ┌──────────────────────────────────────────────────┐  │
│  │  5. Response Handling                             │  │
│  │     - Transform responses                        │  │
│  │     - Handle errors                              │  │
│  │     - Cache responses                            │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────┬────────────────────────────────────┘
                     │ HTTP Request
                     │ (with tenant context)
                     ▼
┌─────────────────────────────────────────────────────────┐
│              Backend APIs (FastAPI)                     │
│  - Tenant-aware endpoints                               │
│  - Per-tenant data isolation                            │
└─────────────────────────────────────────────────────────┘

Component Structure

apps/bff/src/
├── trpc/
│   ├── router.ts              # Main router
│   ├── context.ts              # Context with tenant
│   └── middleware.ts           # Tenant middleware
├── middleware/
│   ├── tenant.ts               # Tenant extraction
│   ├── validation.ts           # Tenant validation
│   └── error-handler.ts        # Error handling
├── routers/
│   ├── tenant.router.ts       # Tenant config router
│   ├── auth.router.ts          # Auth router (tenant-aware)
│   ├── users.router.ts         # Users router (tenant-aware)
│   └── cats.router.ts          # CATs router (tenant-aware)
├── services/
│   ├── fastapi-client.ts      # FastAPI client (tenant-aware)
│   ├── tenant-service.ts       # Tenant service
│   └── cache.ts                # Cache service
└── utils/
    ├── tenant.ts               # Tenant utilities
    └── logger.ts               # Logging

Tenant Context Extraction

Extraction Methods (Priority Order)

1. Subdomain Extraction

Pattern: {tenantId}.bast.ai

Implementation:

File: apps/bff/src/middleware/tenant.ts

import { CreateExpressContextOptions } from '@trpc/server/adapters/express';

export function extractTenantFromSubdomain(req: CreateExpressContextOptions['req']): string | null {
  const host = req.headers.host;
  if (!host) return null;

  const parts = host.split('.');

  // Skip 'www' and 'app' subdomains
  if (parts.length >= 3 && !['www', 'app'].includes(parts[0])) {
    return parts[0];
  }

  return null;
}

2. Header Extraction

Pattern: X-Tenant-ID header

Implementation:

export function extractTenantFromHeader(req: CreateExpressContextOptions['req']): string | null {
  return req.headers['x-tenant-id'] as string || null;
}

3. Path Extraction

Pattern: /tenant/{tenantId}/...

Implementation:

export function extractTenantFromPath(req: CreateExpressContextOptions['req']): string | null {
  const url = req.url || '';
  const match = url.match(/^\/tenant\/([^\/]+)/);
  return match ? match[1] : null;
}

4. JWT Token Extraction

Pattern: Tenant ID in JWT claims

Implementation:

import { verifyToken } from '../utils/auth';

export async function extractTenantFromToken(
  req: CreateExpressContextOptions['req']
): Promise<string | null> {
  const authHeader = req.headers.authorization;
  if (!authHeader) return null;

  const token = authHeader.replace('Bearer ', '');
  try {
    const decoded = await verifyToken(token);
    return decoded.tenantId || decoded.organization || null;
  } catch {
    return null;
  }
}

5. Combined Extraction Function

export async function extractTenantId(
  req: CreateExpressContextOptions['req']
): Promise<string | null> {
  // Try methods in priority order
  const tenantId =
    extractTenantFromSubdomain(req) ||
    extractTenantFromHeader(req) ||
    extractTenantFromPath(req) ||
    await extractTenantFromToken(req);

  return tenantId;
}

Tenant Middleware

Tenant Context Middleware

File: apps/bff/src/trpc/middleware.ts

import { TRPCError } from '@trpc/server';
import { t } from './router';
import { extractTenantId } from '../middleware/tenant';
import { validateTenantAccess } from '../services/tenant-service';
import { logger } from '../utils/logger';

export const tenantContext = t.middleware(async ({ ctx, next }) => {
  // Extract tenant ID
  const tenantId = await extractTenantId(ctx.req);

  if (!tenantId) {
    logger.warn('No tenant ID found in request', {
      host: ctx.req.headers.host,
      path: ctx.req.url,
    });

    throw new TRPCError({
      code: 'BAD_REQUEST',
      message: 'Tenant ID is required. Provide via subdomain, header, path, or JWT token.',
    });
  }

  // Validate tenant exists and user has access
  const validation = await validateTenantAccess(tenantId, ctx.user);

  if (!validation.valid) {
    logger.warn('Tenant access denied', {
      tenantId,
      userId: ctx.user?.id,
      reason: validation.reason,
    });

    throw new TRPCError({
      code: validation.reason === 'NOT_FOUND' ? 'NOT_FOUND' : 'FORBIDDEN',
      message: validation.reason === 'NOT_FOUND'
        ? `Tenant '${tenantId}' not found`
        : `Access denied for tenant '${tenantId}'`,
    });
  }

  // Load tenant configuration
  const tenantConfig = await getTenantConfig(tenantId);

  // Add tenant context to context
  return next({
    ctx: {
      ...ctx,
      tenantId,
      tenant: tenantConfig,
    },
  });
});

Tenant Validation Service

File: apps/bff/src/services/tenant-service.ts

import { fastAPIClient } from './fastapi-client';
import { User } from '../trpc/context';
import { logger } from '../utils/logger';

export interface TenantValidationResult {
  valid: boolean;
  reason?: 'NOT_FOUND' | 'FORBIDDEN' | 'INACTIVE' | 'SUSPENDED';
  tenant?: any;
}

export async function validateTenantAccess(
  tenantId: string,
  user?: User
): Promise<TenantValidationResult> {
  try {
    // Check if tenant exists
    const tenant = await fastAPIClient.get(
      'userManagement',
      `/api/v1/tenants/${tenantId}`,
      {},
      tenantId // Pass tenant ID for routing
    );

    if (!tenant) {
      return { valid: false, reason: 'NOT_FOUND' };
    }

    // Check tenant status
    if (tenant.status === 'inactive') {
      return { valid: false, reason: 'INACTIVE', tenant };
    }

    if (tenant.status === 'suspended') {
      return { valid: false, reason: 'SUSPENDED', tenant };
    }

    // If user is provided, check access
    if (user) {
      const hasAccess = await checkUserTenantAccess(user.id, tenantId);
      if (!hasAccess) {
        return { valid: false, reason: 'FORBIDDEN', tenant };
      }
    }

    return { valid: true, tenant };
  } catch (error: any) {
    logger.error('Tenant validation error', { tenantId, error: error.message });

    if (error.response?.status === 404) {
      return { valid: false, reason: 'NOT_FOUND' };
    }

    throw error;
  }
}

async function checkUserTenantAccess(userId: string, tenantId: string): Promise<boolean> {
  try {
    const user = await fastAPIClient.get(
      'userManagement',
      `/api/v1/users/${userId}`,
      {},
      tenantId
    );

    // Check if user belongs to tenant
    return user.organization === tenantId || user.tenants?.includes(tenantId);
  } catch {
    return false;
  }
}

export async function getTenantConfig(tenantId: string): Promise<any> {
  try {
    const config = await fastAPIClient.get(
      'userManagement',
      `/api/v1/tenants/${tenantId}/config`,
      {},
      tenantId
    );

    return config;
  } catch (error) {
    logger.error('Failed to load tenant config', { tenantId, error });
    throw error;
  }
}

Tenant Router

Tenant Configuration Router

File: apps/bff/src/routers/tenant.router.ts

import { router, protectedProcedure } from '../trpc/router';
import { tenantContext } from '../trpc/middleware';
import { fastAPIClient } from '../services/fastapi-client';
import { cache } from '../services/cache';
import { logger } from '../utils/logger';

export const tenantRouter = router({
  getConfig: protectedProcedure
    .use(tenantContext)
    .query(async ({ ctx }) => {
      const cacheKey = `tenant:config:${ctx.tenantId}`;

      // Try cache first
      const cached = await cache.get(cacheKey);
      if (cached) {
        logger.debug('Tenant config cache hit', { tenantId: ctx.tenantId });
        return cached;
      }

      // Load from backend
      const config = await fastAPIClient.get(
        'userManagement',
        `/api/v1/tenants/${ctx.tenantId}/config`,
        {},
        ctx.tenantId
      );

      // Cache for 5 minutes
      await cache.set(cacheKey, config, 300);

      return {
        tenantId: ctx.tenantId,
        name: config.name,
        slug: config.slug,
        branding: config.branding,
        features: config.features,
        cats: config.cats || [],
        environments: config.environments || ['dev', 'stage', 'prod'],
        limits: config.limits,
        subscription: config.subscription,
      };
    }),

  listCats: protectedProcedure
    .use(tenantContext)
    .query(async ({ ctx }) => {
      const cacheKey = `tenant:cats:${ctx.tenantId}`;

      const cached = await cache.get(cacheKey);
      if (cached) {
        return cached;
      }

      const cats = await fastAPIClient.get(
        'userManagement',
        `/api/v1/tenants/${ctx.tenantId}/cats`,
        {},
        ctx.tenantId
      );

      await cache.set(cacheKey, cats, 300);

      return cats;
    }),

  getCat: protectedProcedure
    .use(tenantContext)
    .input(z.object({ catPrefix: z.string() }))
    .query(async ({ ctx, input }) => {
      // Verify CAT belongs to tenant
      const cats = await fastAPIClient.get(
        'userManagement',
        `/api/v1/tenants/${ctx.tenantId}/cats`,
        {},
        ctx.tenantId
      );

      const cat = cats.find((c: any) => c.prefix === input.catPrefix);
      if (!cat) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: `CAT '${input.catPrefix}' not found for tenant '${ctx.tenantId}'`,
        });
      }

      return cat;
    }),

  updateConfig: protectedProcedure
    .use(tenantContext)
    .input(z.object({
      branding: z.object({
        logo: z.string().optional(),
        colors: z.object({
          primary: z.string().optional(),
          secondary: z.string().optional(),
        }).optional(),
      }).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const updated = await fastAPIClient.put(
        'userManagement',
        `/api/v1/tenants/${ctx.tenantId}/config`,
        input,
        {},
        ctx.tenantId
      );

      // Invalidate cache
      await cache.invalidate(`tenant:config:${ctx.tenantId}`);

      logger.info('Tenant config updated', {
        tenantId: ctx.tenantId,
        updatedBy: ctx.user?.id,
      });

      return updated;
    }),
});

FastAPI Client Integration

Tenant-Aware FastAPI Client

File: apps/bff/src/services/fastapi-client.ts (UPDATE)

import axios, { AxiosInstance, AxiosError } from 'axios';
import { logger } from '../utils/logger';

export class FastAPIClient {
  private clients: Map<string, AxiosInstance> = new Map();

  constructor() {
    // Initialize clients for each FastAPI service
    const services: Record<string, { baseURL: string; timeout?: number }> = {
      security: {
        baseURL: process.env.FASTAPI_SECURITY_URL || 'http://localhost:8000',
        timeout: 10000,
      },
      userManagement: {
        baseURL: process.env.FASTAPI_USER_MGMT_URL || 'http://localhost:8001',
        timeout: 10000,
      },
      cat: {
        baseURL: process.env.FASTAPI_CAT_URL || 'http://localhost:8002',
        timeout: 10000,
      },
      // ... other services
    };

    Object.entries(services).forEach(([name, config]) => {
      const client = axios.create({
        baseURL: config.baseURL,
        timeout: config.timeout,
        headers: {
          'Content-Type': 'application/json',
        },
      });

      // Request interceptor for logging
      client.interceptors.request.use(
        (config) => {
          logger.debug(`FastAPI Request: ${name} ${config.method?.toUpperCase()} ${config.url}`);
          return config;
        },
        (error) => {
          logger.error(`FastAPI Request Error: ${name}`, error);
          return Promise.reject(error);
        }
      );

      // Response interceptor for logging
      client.interceptors.response.use(
        (response) => {
          logger.debug(`FastAPI Response: ${name} ${response.status}`);
          return response;
        },
        (error) => {
          logger.error(`FastAPI Response Error: ${name}`, error);
          return Promise.reject(error);
        }
      );

      this.clients.set(name, client);
    });
  }

  async get(
    service: string,
    endpoint: string,
    config?: any,
    tenantId?: string,
    catPrefix?: string
  ) {
    const client = this.clients.get(service);
    if (!client) {
      throw new Error(`Unknown service: ${service}`);
    }

    // Add tenant context to headers
    const headers = {
      ...config?.headers,
      ...(tenantId && { 'X-Tenant-ID': tenantId }),
      ...(catPrefix && { 'X-CAT-Prefix': catPrefix }),
    };

    try {
      const response = await client.get(endpoint, {
        ...config,
        headers,
      });
      return response.data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw this.handleError(error, service, endpoint);
      }
      throw error;
    }
  }

  async post(
    service: string,
    endpoint: string,
    data?: any,
    config?: any,
    tenantId?: string,
    catPrefix?: string
  ) {
    const client = this.clients.get(service);
    if (!client) {
      throw new Error(`Unknown service: ${service}`);
    }

    const headers = {
      ...config?.headers,
      ...(tenantId && { 'X-Tenant-ID': tenantId }),
      ...(catPrefix && { 'X-CAT-Prefix': catPrefix }),
    };

    try {
      const response = await client.post(endpoint, data, {
        ...config,
        headers,
      });
      return response.data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw this.handleError(error, service, endpoint);
      }
      throw error;
    }
  }

  async put(
    service: string,
    endpoint: string,
    data?: any,
    config?: any,
    tenantId?: string,
    catPrefix?: string
  ) {
    // Similar to post
  }

  async delete(
    service: string,
    endpoint: string,
    config?: any,
    tenantId?: string,
    catPrefix?: string
  ) {
    // Similar to get
  }

  private handleError(error: AxiosError, service: string, endpoint: string) {
    if (error.response) {
      const status = error.response.status;
      const data = error.response.data;
      logger.error(`FastAPI Error: ${service} ${endpoint} - ${status}`, data);

      return new Error(`FastAPI Error (${status}): ${JSON.stringify(data)}`);
    } else if (error.request) {
      logger.error(`FastAPI Unreachable: ${service} ${endpoint}`, error.message);
      return new Error(`FastAPI service unreachable: ${service}`);
    } else {
      logger.error(`FastAPI Request Error: ${service} ${endpoint}`, error.message);
      return new Error(`Request error: ${error.message}`);
    }
  }
}

export const fastAPIClient = new FastAPIClient();

Security & Isolation

Tenant Isolation Enforcement

  1. Always Validate Tenant

    // Every procedure must use tenantContext middleware
    export const myRouter = router({
      myEndpoint: protectedProcedure
        .use(tenantContext) // Required!
        .query(async ({ ctx }) => {
          // ctx.tenantId is guaranteed to exist
        }),
    });
    

  2. Never Trust Client

    // Always use tenantId from context, not from input
    export const myRouter = router({
      getData: protectedProcedure
        .use(tenantContext)
        .input(z.object({ id: z.string() }))
        .query(async ({ ctx, input }) => {
          // Use ctx.tenantId, not input.tenantId
          return fastAPIClient.get(
            'service',
            `/data/${input.id}`,
            {},
            ctx.tenantId // From context, not input
          );
        }),
    });
    

  3. Validate Tenant Access

    // Check user has access to tenant
    if (ctx.user && !hasTenantAccess(ctx.user, ctx.tenantId)) {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'Access denied',
      });
    }
    

Security Best Practices

// 1. Log all tenant operations
logger.info('Tenant operation', {
  tenantId: ctx.tenantId,
  userId: ctx.user?.id,
  operation: 'getData',
});

// 2. Rate limiting per tenant
const rateLimiter = rateLimit({
  keyGenerator: (req) => `tenant:${req.tenantId}`,
  windowMs: 15 * 60 * 1000,
  max: 100,
});

// 3. Audit trail
await auditLog.create({
  tenantId: ctx.tenantId,
  userId: ctx.user?.id,
  action: 'data_access',
  resource: 'users',
  timestamp: new Date(),
});

Error Handling

Tenant-Specific Error Handling

File: apps/bff/src/middleware/error-handler.ts

import { TRPCError } from '@trpc/server';

export function handleTenantError(error: any, tenantId?: string): TRPCError {
  if (error.response?.status === 404) {
    return new TRPCError({
      code: 'NOT_FOUND',
      message: tenantId
        ? `Resource not found for tenant '${tenantId}'`
        : 'Resource not found',
    });
  }

  if (error.response?.status === 403) {
    return new TRPCError({
      code: 'FORBIDDEN',
      message: 'Access denied',
    });
  }

  if (error.response?.status === 401) {
    return new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Authentication required',
    });
  }

  return new TRPCError({
    code: 'INTERNAL_SERVER_ERROR',
    message: error.message || 'Internal server error',
  });
}

Caching Strategy

Tenant-Aware Caching

// Cache key includes tenant ID
const cacheKey = `tenant:${tenantId}:${resource}:${id}`;

// Cache invalidation per tenant
await cache.invalidate(`tenant:${tenantId}:*`);

// Cache TTL per tenant tier
const ttl = tenant.tier === 'enterprise' ? 600 : 300; // 10min vs 5min

Implementation Details

Step-by-Step Implementation

Phase 1: Tenant Context (Week 1-2)

  1. Create Tenant Middleware
  2. Create middleware/tenant.ts
  3. Implement tenant extraction functions
  4. Create trpc/middleware.ts with tenantContext

  5. Update Context

  6. Update trpc/context.ts to include tenant
  7. Add tenant validation
  8. Add tenant config loading

Phase 2: Tenant Service (Week 3-4)

  1. Create Tenant Service
  2. Create services/tenant-service.ts
  3. Implement tenant validation
  4. Implement tenant config loading

  5. Update FastAPI Client

  6. Add tenant ID parameter
  7. Add tenant headers
  8. Update all methods

Phase 3: Tenant Router (Week 5-6)

  1. Create Tenant Router
  2. Create routers/tenant.router.ts
  3. Implement tenant config endpoints
  4. Implement CAT endpoints

  5. Update Main Router

  6. Add tenant router
  7. Update all routers to use tenantContext

Testing Strategy

Unit Tests

// tenant.middleware.test.ts
describe('Tenant Middleware', () => {
  it('extracts tenant from subdomain', () => {
    const req = { headers: { host: 'acme.bast.ai' } };
    const tenantId = extractTenantFromSubdomain(req);
    expect(tenantId).toBe('acme');
  });

  it('validates tenant access', async () => {
    const result = await validateTenantAccess('acme', mockUser);
    expect(result.valid).toBe(true);
  });
});

Integration Tests

// tenant.integration.test.ts
describe('Tenant Integration', () => {
  it('loads tenant config', async () => {
    const result = await trpc.tenant.getConfig.query({ tenantId: 'acme' });
    expect(result.tenantId).toBe('acme');
  });
});

Deployment

Environment Variables

# FastAPI Services
FASTAPI_SECURITY_URL=http://localhost:8000
FASTAPI_USER_MGMT_URL=http://localhost:8001

# Cache
REDIS_URL=redis://localhost:6379

# Logging
LOG_LEVEL=info

Health Check

app.get('/health', async (req, res) => {
  const health = {
    status: 'ok',
    tenantAware: true,
    services: {
      fastapi: await checkFastAPIConnection(),
      cache: await checkCacheConnection(),
    },
  };
  res.json(health);
});

Document created: December 16, 2025
Component: BFF Tenant Aware