Problem
lib-streaming v1.5 enforces contract.ErrMissingTenantID at preflight for every non-system event:
internal/producer/preflight.go:60-61
internal/contract/outbox_envelope.go:207-208
Every catalog entry has SystemEvent: false, so any emit with empty EmitRequest.TenantID is rejected.
This blocks plugins running in single-tenant mode — where the architectural directive is to NOT require/propagate tenant identity anywhere in the application. The tenant concept is meaningful only when isolation between clients exists (i.e., MT mode).
Concrete failure
plugin-br-bank-transfer running in ST mode hit the following runtime error during TED IN flow:
ted_in.processor.hard_failure
error: "emit ted in undeliverable streaming event: emit streaming event
transfer_incoming.undeliverable: streaming tenant id missing from context"
The plugin's tenant context is intentionally empty in ST (the rest of the codebase uses tmcore.GetTenantIDContext which returns empty string and treats that as a graceful signal). lib-streaming is the only layer that escalates empty tenant to a hard error.
Workaround used by the plugin
plugin-br-bank-transfer now skips emit entirely when tmcore.GetTenantIDContext(ctx) == "" and records a metric btf.streaming.emit_total{outcome=skipped_no_tenant}. This is a coverage gap — streaming events are not published in ST mode, breaking any downstream consumer that depends on them (audit, analytics, dashboards).
Proposed solution — auto-detect mode via MULTI_TENANT_ENABLED env
The Lerian platform already uses MULTI_TENANT_ENABLED as the canonical mode signal — every plugin reads it, lib-systemplane reads it via WithMultiTenantEnabled, lib-commons tenant-manager wiring branches on it. lib-streaming should read the same env and adjust preflight automatically.
Behavior contract
MULTI_TENANT_ENABLED |
Preflight on empty TenantID |
true |
Reject (current behavior) — preserves MT tenant isolation guarantees |
false or unset |
Accept empty TenantID, route to configured destination |
Why this is better than a producer option
- Zero configuration drift: plugins don't need to remember to pass
WithSingleTenantMode() when they already set MULTI_TENANT_ENABLED=false
- Consistent platform convention: matches lib-systemplane, lib-commons tenant-manager, every plugin bootstrap
- No double source of truth: deployment mode is set in ONE place, all libs read it
- No new API surface: existing producer constructors stay unchanged
Implementation sketch
// In producer initialization:
multiTenantEnabled := strings.EqualFold(os.Getenv("MULTI_TENANT_ENABLED"), "true")
// In preflight:
if req.TenantID == "" {
if multiTenantEnabled {
return contract.ErrMissingTenantID // current behavior, MT requires tenant
}
// ST mode: route to default destination, no error
}
Or expose the producer-internal flag for explicit override in tests (WithMultiTenantEnabled(bool)), but default behavior reads the env.
Affected plugins
plugin-br-bank-transfer (this repo's caller) — hit the failure
- Any other plugin running in ST mode that emits non-system streaming events will face the same issue.
Workaround code reference
For context, the plugin's current skip-if-empty workaround:
https://github.com/LerianStudio/plugin-br-bank-transfer/blob/refactor/multi-tenant-license-alignment/internal/bank_transfer/streaming/emitter.go (commit 99e480dc)
tenantID := tenantIDFromContext(ctx)
if tenantID == "" {
// ST mode: lib-streaming preflight requires TenantID; skip emit
// until upstream supports single-tenant deployments.
streamingMetricsRegistry.recordOutcome(ctx, definitionKey, streamingOutcomeSkippedNoTenant)
return nil
}
Acceptance criteria
Context
This came up while aligning plugin-br-bank-transfer's tenant handling with the Midaz canonical pattern. The plugin codebase was successfully purged of all custom strict tenant gates; lib-streaming is now the only remaining mandatory tenant requirement, and it cannot be relaxed at the plugin level without losing the streaming-event contract for ST deployments.
The MULTI_TENANT_ENABLED env is already the platform-wide mode signal — adopting it here means every Lerian plugin gets ST streaming support automatically once they configure their deployment mode.
Problem
lib-streamingv1.5 enforcescontract.ErrMissingTenantIDat preflight for every non-system event:internal/producer/preflight.go:60-61internal/contract/outbox_envelope.go:207-208Every catalog entry has
SystemEvent: false, so any emit with emptyEmitRequest.TenantIDis rejected.This blocks plugins running in single-tenant mode — where the architectural directive is to NOT require/propagate tenant identity anywhere in the application. The tenant concept is meaningful only when isolation between clients exists (i.e., MT mode).
Concrete failure
plugin-br-bank-transferrunning in ST mode hit the following runtime error during TED IN flow:The plugin's tenant context is intentionally empty in ST (the rest of the codebase uses
tmcore.GetTenantIDContextwhich returns empty string and treats that as a graceful signal). lib-streaming is the only layer that escalates empty tenant to a hard error.Workaround used by the plugin
plugin-br-bank-transfernow skips emit entirely whentmcore.GetTenantIDContext(ctx) == ""and records a metricbtf.streaming.emit_total{outcome=skipped_no_tenant}. This is a coverage gap — streaming events are not published in ST mode, breaking any downstream consumer that depends on them (audit, analytics, dashboards).Proposed solution — auto-detect mode via
MULTI_TENANT_ENABLEDenvThe Lerian platform already uses
MULTI_TENANT_ENABLEDas the canonical mode signal — every plugin reads it, lib-systemplane reads it viaWithMultiTenantEnabled, lib-commons tenant-manager wiring branches on it. lib-streaming should read the same env and adjust preflight automatically.Behavior contract
MULTI_TENANT_ENABLEDTenantIDtruefalseor unsetTenantID, route to configured destinationWhy this is better than a producer option
WithSingleTenantMode()when they already setMULTI_TENANT_ENABLED=falseImplementation sketch
Or expose the producer-internal flag for explicit override in tests (
WithMultiTenantEnabled(bool)), but default behavior reads the env.Affected plugins
plugin-br-bank-transfer(this repo's caller) — hit the failureWorkaround code reference
For context, the plugin's current skip-if-empty workaround:
https://github.com/LerianStudio/plugin-br-bank-transfer/blob/refactor/multi-tenant-license-alignment/internal/bank_transfer/streaming/emitter.go (commit 99e480dc)
Acceptance criteria
MULTI_TENANT_ENABLEDenv at init timeMULTI_TENANT_ENABLED=true, preflight rejects emptyTenantID(current behavior unchanged)MULTI_TENANT_ENABLED=falseor unset, preflight accepts emptyTenantIDand routes to configured destinationWithMultiTenantEnabled(bool)for test override / explicit configurationContext
This came up while aligning plugin-br-bank-transfer's tenant handling with the Midaz canonical pattern. The plugin codebase was successfully purged of all custom strict tenant gates; lib-streaming is now the only remaining mandatory tenant requirement, and it cannot be relaxed at the plugin level without losing the streaming-event contract for ST deployments.
The
MULTI_TENANT_ENABLEDenv is already the platform-wide mode signal — adopting it here means every Lerian plugin gets ST streaming support automatically once they configure their deployment mode.