MATIH Platform is in active MVP development. Documentation reflects current implementation status.
2. Architecture
Tenant Context Propagation

Tenant Context Propagation

The TenantContextHolder is the central mechanism that ensures every operation executes within the correct tenant boundary. It uses thread-local storage (Java) or context variables (Python) to carry tenant identity through the entire request processing chain.


Java Implementation

TenantContextHolder

public final class TenantContextHolder {
    private static final ThreadLocal<TenantContext> CONTEXT = new ThreadLocal<>();
 
    public static void setTenantId(String tenantId) {
        CONTEXT.set(new TenantContext(tenantId, null, Map.of()));
    }
 
    public static Optional<String> getTenantId() {
        return getContext().map(TenantContext::tenantId);
    }
 
    public static String requireTenantId() {
        return requireContext().tenantId();
    }
 
    public static void clear() {
        CONTEXT.remove();
    }
}

TenantContext Record

public record TenantContext(
    String tenantId,
    String userId,
    Map<String, Object> attributes
) {
    public TenantContext {
        if (tenantId == null || tenantId.isBlank()) {
            throw new IllegalArgumentException("Tenant ID cannot be null or blank");
        }
        attributes = attributes != null ? Map.copyOf(attributes) : Map.of();
    }
}

Context Setting

The context is set from three sources:

SourceScenarioCode
JWT tokenStandard requestTenantContextHolder.setTenant(tenantId, userId)
HTTP headerInter-service callExtract X-Tenant-ID from request
ProgrammaticBackground jobsTenantContextHolder.setContext(new TenantContext(...))

Context Propagation to Child Threads

When work is dispatched to thread pools, the context must be explicitly propagated:

// Wrap a Runnable to preserve context
Runnable wrapped = TenantContextHolder.wrapWithContext(() -> {
    processInBackground();
});
executorService.submit(wrapped);

The wrapper captures the context at wrap time and restores it at execution time.


Python Implementation

Python services use contextvars for async-compatible context:

from contextvars import ContextVar
 
_tenant_id: ContextVar[str] = ContextVar('tenant_id')
 
def get_tenant_id() -> str:
    return _tenant_id.get()
 
def set_tenant_id(tenant_id: str) -> None:
    _tenant_id.set(tenant_id)

Python's contextvars automatically propagate through asyncio task chains.


Inter-Service Propagation

Tenant context propagates via HTTP headers:

HeaderPurpose
X-Tenant-IDTenant identifier
X-User-IDAuthenticated user

For Kafka events, context propagates as message headers and payload fields.


Security Invariants

InvariantEnforcement
No context = no data accessrequireTenantId() throws IllegalStateException
Context always clearedfinally block in filter chain calls clear()
Queries always filteredRepository uses requireTenantId() for every query
Cross-tenant access loggedMismatch triggers security alert

Related Pages