Skip to content

Support single-tenant mode without requiring TenantID at preflight #5

@jeffersonrodrigues92

Description

@jeffersonrodrigues92

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

  • Producer reads MULTI_TENANT_ENABLED env at init time
  • When MULTI_TENANT_ENABLED=true, preflight rejects empty TenantID (current behavior unchanged)
  • When MULTI_TENANT_ENABLED=false or unset, preflight accepts empty TenantID and routes to configured destination
  • Idempotency key construction is documented for ST mode (how empty tenant is handled)
  • Existing MT tests pass without modification
  • New tests cover the ST producer path
  • Optional: expose WithMultiTenantEnabled(bool) for test override / explicit configuration

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or improvement request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions