Microservice Propagation
Forward the current tenant context to downstream services using propagateTenantHeaders(). Works with any HTTP client — zero dependencies.
import { propagateTenantHeaders } from '@nestarc/tenancy';
// With fetch
const res = await fetch('http://orders-service/api/orders', {
headers: { 'Content-Type': 'application/json', ...propagateTenantHeaders() },
});
// With axios
const res = await axios.get('http://orders-service/api/orders', {
headers: propagateTenantHeaders(),
});
// With @nestjs/axios HttpService
this.httpService.get('http://orders-service/api/orders', {
headers: propagateTenantHeaders(),
});By default, the function uses X-Tenant-Id as the header name. Pass a custom name if needed:
propagateTenantHeaders('X-Custom-Tenant'); // { 'X-Custom-Tenant': 'tenant-abc' }Returns an empty object {} when no tenant context is available (e.g., outside a request or inside withoutTenant()).
How it works:
propagateTenantHeaders()reads from the same staticAsyncLocalStorageused byTenancyContext. No dependency injection required — it works anywhere in the call stack.
For more control, use HttpTenantPropagator directly:
import { HttpTenantPropagator, TenancyContext } from '@nestarc/tenancy';
const propagator = new HttpTenantPropagator(new TenancyContext(), {
headerName: 'X-Tenant-Id',
});
const headers = propagator.getHeaders(); // { 'X-Tenant-Id': 'tenant-abc' }Message Queue & RPC Propagation
Transport-specific propagators for Bull, Kafka, and gRPC. All use structural typing with zero runtime dependencies on transport packages.
Bull (BullMQ)
import { BullTenantPropagator, TenancyContext } from '@nestarc/tenancy';
const propagator = new BullTenantPropagator(new TenancyContext());
// Producer: inject tenant into job data
await queue.add('process-order', propagator.inject({ orderId: '123' }));
// → { orderId: '123', __tenantId: 'tenant-abc' }
// Consumer: extract tenant from job data
const tenantId = propagator.extract(job.data); // 'tenant-abc'Kafka
import { KafkaTenantPropagator, TenancyContext } from '@nestarc/tenancy';
const propagator = new KafkaTenantPropagator(new TenancyContext());
// Producer: inject tenant into message headers
await producer.send({
topic: 'orders',
messages: [propagator.inject({ value: JSON.stringify(payload) })],
});
// Consumer: extract tenant from message
const tenantId = propagator.extract(message); // handles string & Buffer headersgRPC
import { GrpcTenantPropagator, TenancyContext } from '@nestarc/tenancy';
const propagator = new GrpcTenantPropagator(new TenancyContext());
// Client: inject tenant into metadata
const metadata = new Metadata();
propagator.inject(metadata); // sets 'x-tenant-id' key
// Server: extract tenant from metadata
const tenantId = propagator.extract(call.metadata);Inbound Context Restoration
TenantContextInterceptor automatically restores tenant context from incoming microservice messages. It wraps handler execution in TenancyContext.run().
import { TenantContextInterceptor, TenancyContext } from '@nestarc/tenancy';
// Recommended: specify transport explicitly to avoid duck-typing ambiguity
app.useGlobalInterceptors(
new TenantContextInterceptor(new TenancyContext(), { transport: 'kafka' }),
);Supported transports: 'kafka' | 'bull' | 'grpc'.
HTTP is skipped —
TenantMiddleware+TenancyGuardalready handle HTTP tenant extraction. The interceptor is designed for RPC transports only.
| Option | Type | Default | Description |
|---|---|---|---|
transport | 'kafka' | 'bull' | 'grpc' | auto-detect | Explicit transport selection (recommended) |
kafkaHeaderName | string | 'X-Tenant-Id' | Kafka message header name |
bullDataKey | string | '__tenantId' | Bull job data key |
grpcMetadataKey | string | 'x-tenant-id' | gRPC metadata key |
Missing-context policy
Non-HTTP paths previously had to decide independently what to do when no tenant context was present. Version 0.15 adds one module-level policy for BullMQ, Kafka, gRPC, response cache, Redis, and search integration paths:
TenancyModule.forRoot({
tenantExtractor: 'X-Tenant-Id',
missingContext: { policy: 'warn' }, // 'ignore' | 'warn' | 'throw'
});ignorepreserves the earlier silent/pass-through behavior and is the default.warnreports the missing context and lets the integration use its documented empty result.throwreports and raisesTenantContextMissingErrorbefore the resource is accessed.
When constructing a propagator directly, inject the configured diagnostics instance and attach a stable, low-cardinality resource name:
import {
BullTenantPropagator,
TenantContextDiagnostics,
TenancyContext,
} from '@nestarc/tenancy';
const diagnostics = app.get(TenantContextDiagnostics);
const propagator = new BullTenantPropagator(new TenancyContext(), {
diagnostics,
resource: 'orders',
});Both warn and throw emit tenant.context_missing, add the same event to the active OpenTelemetry span, and increment nestarc.tenancy.missing_context. The bounded attributes are tenant.transport, tenant.operation, and optional tenant.resource. Reporter failures are isolated from the context decision.
HTTP extraction is intentionally outside this policy because TenantMiddleware and TenancyGuard already define its fail-closed contract. See Non-HTTP Resources for Redis keys and vendor-neutral search adapters using the same diagnostics boundary.
Validate inbound tenant IDs in 0.16
import {
TenantContextDiagnostics,
TenantContextInterceptor,
TenancyContext,
type TenantIdValidator,
} from '@nestarc/tenancy';
const validateTenantId: TenantIdValidator = (id) => /^org_[a-z0-9-]+$/.test(id);
app.useGlobalInterceptors(new TenantContextInterceptor(new TenancyContext(), {
transport: 'kafka',
validateTenantId,
diagnostics: app.get(TenantContextDiagnostics),
resource: 'orders',
}));The validator may return a boolean or a promise. A false result raises BadRequestException before restoring context or running the handler. Invalid-context diagnostics expose transport, operation, and optional stable resource labels, never the rejected ID or carrier contents; the event is tenant.context_invalid and metric is nestarc.tenancy.invalid_context.
Without an explicit validator, RPC preserves its non-empty-string behavior during 0.x. HTTP's UUID-like default is planned for RPC in 1.0. Missing-context policy does not weaken invalid-ID rejection. Authenticate the producer/channel and authorize its tenant claim separately; format validation is not authorization.