Multi-Tenancy Guide

Last updated: April 28, 2026 Document type: Integration guide

This guide covers patterns for using the BriteCore SDK across multiple sites/tenants in a single application or service.


Overview

The SDK is designed to support multi-tenancy through:

  1. Module-level api_client proxy — Lazy-initialized singleton

  2. use_api_client(client) context manager — Bind wrapper calls to a specific client

  3. reset_api_client() function — Clear the module-level client (for site switching)

  4. Explicit client instantiation — Create independent BritecoreAPIClient instances per site

  5. Thread-safe initialization — Each site gets its own HTTP connection pool


Pattern 1: Module-Level Client with Reset (Simplest)

Use the shared module-level client and reset it when switching sites. Best for sequential site operations.

from britecore_sdk.api.api_calls import get_api_client, reset_api_client
from britecore_sdk.api.api_calls.v2 import policies

# Site 1: Initialize client for production
api_client = get_api_client()  # Auto-loads config for 'production' from settings
result1 = policies.retrieve_policy(policy_number="POL001")
print(f"Production: {result1['data']['policy_number']}")

# Switch to staging
reset_api_client()  # Clear the module-level client
import os
os.environ["target_site"] = "staging"  # Change site
api_client = get_api_client()  # Reinitialize for new site
result2 = policies.retrieve_policy(policy_number="POL002")
print(f"Staging: {result2['data']['policy_number']}")

# Back to production
reset_api_client()
del os.environ["target_site"]  # or restore original value
api_client = get_api_client()  # Back to production

Pros:

  • Simplest pattern

  • Reuses lazy initialization

  • Works for script-like operations

Cons:

  • Environmental coupling (requires env var changes or file rewrites)

  • Not ideal for concurrent operations

  • Manual reset management



Pattern 3: Context Manager for Isolated Operations

Use BritecoreAPIClient context manager for guaranteed resource cleanup. Best for long-running services.

from britecore_sdk.api.britecore_api_client import BritecoreAPIClient
from britecore_sdk.api.api_calls.v2 import policies

sites = {
    "production": ("https://api.prod.example.com", "prod-key"),
    "staging": ("https://api.staging.example.com", "staging-key"),
}

# Process each site in isolation
for site_name, (base_url, api_key) in sites.items():
    with BritecoreAPIClient(site_name).init_client(
        base_url=base_url,
        api_key=api_key
    ) as client:
        result = policies.retrieve_policy(policy_number="POL001")
        print(f"{site_name} (via context manager): {result}")
    # urllib3 PoolManager auto-closed on exit

Pros:

  • Guaranteed resource cleanup

  • No connection leaks

  • Very testable

  • Clear site isolation

Cons:

  • Slightly more verbose


Pattern 4: Long-Lived Clients in a Service Registry

For background services/workers, maintain a registry of pre-initialized clients.

from britecore_sdk.api.britecore_api_client import BritecoreAPIClient
from britecore_sdk.api.api_calls.v2 import policies
import threading
from typing import Dict

class SiteClientRegistry:
    """Thread-safe registry of per-site clients."""

    def __init__(self):
        self._clients: Dict[str, BritecoreAPIClient] = {}
        self._lock = threading.Lock()

    def get_or_create(self, site_name: str, base_url: str, api_key: str) -> BritecoreAPIClient:
        """Get or create a client for a site (thread-safe)."""
        if site_name in self._clients:
            return self._clients[site_name]

        with self._lock:
            if site_name not in self._clients:
                client = BritecoreAPIClient(site_name).init_client(
                    base_url=base_url,
                    api_key=api_key
                )
                self._clients[site_name] = client
            return self._clients[site_name]

    def cleanup(self, site_name: str = None):
        """Close a client or all clients."""
        with self._lock:
            if site_name:
                if site_name in self._clients:
                    self._clients[site_name].__exit__(None, None, None)
                    del self._clients[site_name]
            else:
                for client in self._clients.values():
                    client.__exit__(None, None, None)
                self._clients.clear()

# Usage in a background service
registry = SiteClientRegistry()

def process_policy_updates(site_name: str, base_url: str, api_key: str):
    """Background worker that periodically calls an API."""
    client = registry.get_or_create(site_name, base_url, api_key)
    result = policies.retrieve_policy(policy_number="POL001")
    print(f"Processed: {result['data']['policy_number']}")

# Cleanup on shutdown
def on_shutdown():
    registry.cleanup()

Pros:

  • Efficient for long-running services

  • Reuses connections per site

  • Thread-safe

  • Explicit lifecycle control

Cons:

  • Manual cleanup required

  • More complex


Pattern 5: Configuration File per Environment

Store site configs in separate TOML files and switch via BRITECORE_SDK_SETTINGS_FILE.

Directory structure:

config/
├── prod.toml
├── prod.secrets.toml
├── staging.toml
└── staging.secrets.toml

prod.toml:

[default]
target_site = "production"
web_timeout = 10

[production]
# Leave empty — credentials go in prod.secrets.toml

prod.secrets.toml:

[production]
base_url = "https://api.prod.example.com"
api_key = "prod-key-xxx"

Python to switch:

import os
from britecore_sdk.api.api_calls import get_api_client, reset_api_client

# Switch to production
os.environ["BRITECORE_SDK_SETTINGS_FILE"] = "config/prod.toml"
reset_api_client()
client = get_api_client()

# Switch to staging
os.environ["BRITECORE_SDK_SETTINGS_FILE"] = "config/staging.toml"
reset_api_client()
client = get_api_client()

Pros:

  • Environment-specific configs version-controlled

  • Clean separation

  • Easy to deploy

Cons:

  • Still requires file I/O and reset_api_client()

  • Not ideal for concurrent operations


Best Practices

1. Use Explicit Credentials for Independent Sites

# ✅ Good: Each site is independent
prod_client = init_api_client("prod", base_url="...", api_key="...")
staging_client = init_api_client("staging", base_url="...", api_key="...")

result1 = policies.retrieve_policy(policy_number="POL001")  # Uses prod_client
result2 = policies.retrieve_policy(policy_number="POL002")  # Uses staging_client

2. Store Credentials in Environment Variables or Secure Vaults

import os

prod_client = init_api_client(
    "prod",
    base_url=os.environ["PROD_BASE_URL"],
    api_key=os.environ["PROD_API_KEY"],
)

3. Use Context Manager for Request Isolation

# ✅ Good: Guaranteed cleanup
with BritecoreAPIClient("prod").init_client(...) as client:
    result = policies.retrieve_policy(...)
# Pool closed, no leaks

# ❌ Avoid: Manual cleanup
client = init_api_client("prod", ...)
result = policies.retrieve_policy(...)
# No explicit cleanup

4. Don’t Mix Module-Level and Explicit Initialization

# ❌ Avoid: Confusing to read
from britecore_sdk.api.api_calls import get_api_client, init_api_client

api_client = get_api_client()  # Module-level
result1 = policies.retrieve_policy(...)

other_client = init_api_client("other_site", ...)  # Explicit
result2 = policies.retrieve_policy(...)  # Which client is used?

5. Include Site Name in Logs and Traces

import logging

logger = logging.getLogger(__name__)

for site_name, creds in sites.items():
    client = init_api_client(site_name, ...)
    logger.info(f"Processing site: {site_name}", extra={"site": site_name})
    result = policies.retrieve_policy(...)
    logger.info(f"Result for {site_name}: {result['data']}", extra={"site": site_name})

Concurrent Multi-Tenancy with Async

For concurrent operations across sites, use async wrappers with independent clients:

import asyncio
from britecore_sdk.api.britecore_async_api_client import AsyncBritecoreAPIClient
from britecore_sdk.api.api_calls.v2.async_policies import aretrieve_policy

async def fetch_policy_for_site(site_name: str, base_url: str, api_key: str, policy_num: str):
    """Fetch a policy from one site (concurrent-safe)."""
    async with AsyncBritecoreAPIClient(site_name).init_client(
        base_url=base_url,
        api_key=api_key
    ) as client:
        result = await aretrieve_policy(policy_number=policy_num)
        return site_name, result

async def main():
    """Concurrently fetch from multiple sites."""
    sites = {
        "prod": ("https://api.prod.example.com", "prod-key"),
        "staging": ("https://api.staging.example.com", "staging-key"),
    }

    tasks = [
        fetch_policy_for_site(site_name, url, key, "POL001")
        for site_name, (url, key) in sites.items()
    ]

    results = await asyncio.gather(*tasks)
    for site_name, result in results:
        print(f"{site_name}: {result['data']['policy_number']}")

asyncio.run(main())

Testing Multi-Tenancy

import pytest
from britecore_sdk.api.api_calls import init_api_client

@pytest.fixture
def prod_client():
    """Fixture for production site client (dry-run mode)."""
    return init_api_client(
        "prod",
        base_url="https://api.prod.example.com",
        api_key="test-key",
        client_dry_run=True,
    )

@pytest.fixture
def staging_client():
    """Fixture for staging site client (dry-run mode)."""
    return init_api_client(
        "staging",
        base_url="https://api.staging.example.com",
        api_key="test-key",
        client_dry_run=True,
    )

def test_policy_lookup_across_sites(prod_client, staging_client):
    """Test that policy lookup works on both sites."""
    from britecore_sdk.api.api_calls.v2 import policies

    # Dry-run calls — no network, just payload shapes
    prod_result = policies.retrieve_policy(policy_number="POL001")
    staging_result = policies.retrieve_policy(policy_number="POL001")

    assert prod_result["dry_run"] is True
    assert staging_result["dry_run"] is True

Common Patterns by Use Case

Use Case

Pattern

Why

Script processing multiple sites sequentially

Pattern 1 (Module + Reset)

Simple, no file overhead

Microservice with multiple tenants

Pattern 4 (Service Registry)

Reuses connections, thread-safe

Background worker

Pattern 3 (Context Manager)

Clean resource lifecycle

Serverless function (AWS Lambda, etc)

Pattern 2 (Explicit Creds)

Stateless, no file I/O

Long-running API server

Pattern 4 (Registry) or Pattern 3

Connection pooling

Test suite

Pattern 2 (Explicit Creds) + dry-run

Isolation per test


Troubleshooting

“Different sites returning same data”

Cause: Module-level client not reset between site switches

Solution:

from britecore_sdk.api.api_calls import reset_api_client
reset_api_client()  # Clear before switching sites

“Connection pool exhausted”

Cause: Too many long-lived clients without cleanup

Solution:

# Use context manager for cleanup
with BritecoreAPIClient("site").init_client(...) as client:
    result = policies.retrieve_policy(...)
# Cleanup happens automatically

“Site config not found”

Cause: target_site mismatches config section name

Solution:

# Ensure target_site matches a section in settings.toml / .secrets.toml
[staging]  # This is the section name
base_url = "..."
api_key = "..."

# Set target_site to match
os.environ["target_site"] = "staging"

See Also

  • CONFIG_MANAGEMENT.md — Configuration file hierarchy

  • docs/OBSERVABILITY.md — Logging across multiple sites

  • GETTING_STARTED.md — Initial setup