Skip to main content

Tenant Context

Understanding tenant context is crucial for multi-tenant applications using Azotte. This guide explains how to properly manage and utilize tenant information across your integration.

Tenant Architecture

Multi-Tenant Design

Azotte supports a multi-tenant architecture where each tenant represents an independent business entity with isolated data and configurations.

interface Tenant {
id: string;
name: string;
subdomain: string;
created_at: Date;
status: 'active' | 'suspended' | 'pending';
configuration: TenantConfig;
}

interface TenantConfig {
currency: string;
timezone: string;
language: string;
branding: BrandingConfig;
features: FeatureFlags;
}

Tenant Identification

Azotte identifies tenants through multiple methods:

  • Subdomain: tenant-name.azotte.com
  • Custom Domain: billing.yourcompany.com
  • API Key: Tenant-scoped API keys
  • Header: X-Tenant-ID HTTP header

API Context

Authentication Headers

POST /api/subscriptions
Authorization: Bearer sk_live_123abc...
X-Tenant-ID: tenant_456def
Content-Type: application/json

SDK Configuration

import { Azotte } from '@azotte/sdk';

const azotte = new Azotte({
apiKey: 'sk_live_123abc...',
tenantId: 'tenant_456def',
environment: 'production'
});

// All subsequent calls will use this tenant context
const subscription = await azotte.subscriptions.create({
customer_id: 'cus_789ghi',
plan_id: 'plan_basic'
});

Tenant Isolation

Data Segregation

  • Database Level: Separate schemas per tenant
  • API Level: Tenant-scoped endpoints
  • Storage Level: Isolated file systems
  • Cache Level: Tenant-specific cache keys

Security Boundaries

interface TenantSecurity {
data_encryption: {
at_rest: boolean;
in_transit: boolean;
key_management: 'tenant_managed' | 'azotte_managed';
};
access_control: {
rbac_enabled: boolean;
sso_integration: boolean;
ip_restrictions: string[];
};
compliance: {
gdpr_compliant: boolean;
pci_dss: boolean;
soc2: boolean;
};
}

Context Management

Request Context

// Express.js middleware example
const tenantContext = (req, res, next) => {
const tenantId = req.headers['x-tenant-id'] ||
req.subdomain ||
extractFromApiKey(req.headers.authorization);

req.tenant = await loadTenantConfig(tenantId);
next();
};

// Usage in routes
app.use('/api', tenantContext);

app.post('/api/customers', (req, res) => {
const tenant = req.tenant;
// Customer creation scoped to tenant
});

Context Switching

class TenantContextManager {
private currentTenant: string | null = null;

switchTenant(tenantId: string): void {
this.currentTenant = tenantId;
this.updateApiContext();
}

getCurrentTenant(): string | null {
return this.currentTenant;
}

private updateApiContext(): void {
// Update SDK configuration
azotte.setTenantContext(this.currentTenant);
}
}

Configuration Management

Per-Tenant Settings

{
"tenant_id": "tenant_456def",
"configuration": {
"billing": {
"currency": "USD",
"tax_calculation": "inclusive",
"invoice_numbering": "sequential"
},
"features": {
"advanced_analytics": true,
"custom_branding": true,
"api_access": true
},
"limits": {
"max_customers": 10000,
"max_subscriptions": 50000,
"api_rate_limit": 1000
}
}
}

Environment-Specific Context

const getTenantConfig = (environment) => {
const configs = {
development: {
tenantId: 'tenant_dev_123',
apiKey: 'sk_test_...',
webhookUrl: 'http://localhost:3000/webhooks'
},
staging: {
tenantId: 'tenant_staging_456',
apiKey: 'sk_test_...',
webhookUrl: 'https://staging.app.com/webhooks'
},
production: {
tenantId: 'tenant_prod_789',
apiKey: 'sk_live_...',
webhookUrl: 'https://app.com/webhooks'
}
};

return configs[environment];
};

Multi-Tenant Webhooks

Tenant-Specific Endpoints

// Register tenant-specific webhook endpoints
const registerWebhooks = async (tenantId) => {
await azotte.webhooks.create({
url: `https://app.com/webhooks/${tenantId}`,
events: ['subscription.*', 'payment.*'],
tenant_id: tenantId
});
};

Webhook Routing

app.post('/webhooks/:tenantId', (req, res) => {
const tenantId = req.params.tenantId;
const event = req.body;

// Validate webhook signature for this tenant
const isValid = azotte.webhooks.validateSignature(
event,
req.headers['azotte-signature'],
tenantId
);

if (isValid) {
processWebhookEvent(event, tenantId);
res.status(200).send('OK');
} else {
res.status(400).send('Invalid signature');
}
});

Tenant Provisioning

Automatic Provisioning

interface TenantProvisioning {
create_tenant: (config: TenantCreateRequest) => Promise<Tenant>;
setup_billing: (tenantId: string) => Promise<void>;
configure_features: (tenantId: string, features: FeatureFlags) => Promise<void>;
setup_webhooks: (tenantId: string, endpoints: string[]) => Promise<void>;
}

// Example provisioning flow
const provisionNewTenant = async (companyInfo: CompanyInfo) => {
const tenant = await azotte.tenants.create({
name: companyInfo.name,
subdomain: companyInfo.subdomain,
configuration: {
currency: companyInfo.currency,
timezone: companyInfo.timezone
}
});

// Setup default billing configuration
await azotte.billing.setupDefaults(tenant.id);

// Configure webhooks
await azotte.webhooks.create({
tenant_id: tenant.id,
url: `https://${companyInfo.subdomain}.app.com/webhooks`,
events: ['*']
});

return tenant;
};

Testing with Tenant Context

Test Isolation

describe('Subscription Management', () => {
const testTenantId = 'tenant_test_123';

beforeEach(async () => {
// Setup test tenant context
azotte.setTenantContext(testTenantId);

// Clean up test data
await cleanupTenantData(testTenantId);
});

it('should create subscription for tenant', async () => {
const subscription = await azotte.subscriptions.create({
customer_id: 'test_customer',
plan_id: 'test_plan'
});

expect(subscription.tenant_id).toBe(testTenantId);
});
});

Mock Tenant Context

const mockTenantContext = {
tenant_id: 'tenant_mock_123',
configuration: {
currency: 'USD',
features: {
advanced_analytics: true
}
}
};

// Mock SDK calls
jest.mock('@azotte/sdk', () => ({
Azotte: jest.fn().mockImplementation(() => ({
getTenantContext: () => mockTenantContext,
subscriptions: {
create: jest.fn().mockResolvedValue(mockSubscription)
}
}))
}));

Best Practices

Context Validation

  • Always validate tenant ID before processing requests
  • Implement tenant existence checks
  • Use tenant-scoped database queries
  • Log tenant context for debugging

Error Handling

const validateTenantContext = (tenantId) => {
if (!tenantId) {
throw new Error('Tenant context is required');
}

if (!isValidTenantId(tenantId)) {
throw new Error('Invalid tenant ID format');
}

if (!tenantExists(tenantId)) {
throw new Error('Tenant not found');
}
};

Performance Optimization

  • Cache tenant configurations
  • Use tenant-scoped connection pools
  • Implement tenant-aware caching strategies
  • Monitor per-tenant performance metrics

Troubleshooting

Common Issues

  • Missing Tenant Context: API calls without proper tenant identification
  • Cross-Tenant Data Leakage: Incorrect tenant scoping in queries
  • Authentication Errors: Invalid tenant-scoped API keys
  • Configuration Conflicts: Mismatched tenant settings

Debugging Tips

// Enable tenant context logging
azotte.setDebugMode(true);

// Log current tenant context
console.log('Current tenant:', azotte.getCurrentTenant());

// Validate tenant permissions
const permissions = await azotte.tenants.getPermissions();
console.log('Tenant permissions:', permissions);

Next Steps