helm

plugin-br-payments-helm

Chart Contract

A Helm chart for plugin-br-payments — Lerian Studio’s plugin for Brazilian payment operations: boleto issuance, bill payments (bankslip, utilities, DARF), settlement via webhooks, and periodic reconciliation.

TL;DR

helm install my-payments oci://ghcr.io/lerianstudio/plugin-br-payments-helm \
  --version <chart version> \
  --namespace midaz-plugins --create-namespace \
  -f values-prod.yaml

The default image is the stable 1.2.0 release. The health-port compatibility floor remains 1.2.0-beta.1. Pin app.image.tag explicitly in production values.

Prerequisites

Architecture

The chart deploys one Deployment, one process (/app with SERVICE_TYPE=both):

There is no separate worker Deployment — all background work runs inside the API pod. The Dockerfile also ships a standalone /worker binary for split deployments, but the chart does not use it; the unified SERVICE_TYPE=both mode is the operational model agreed with the plugin team.

Storage

The plugin uses PostgreSQL only:

Required configuration

The chart fails fast on helm install if any of the following are missing:

Field Description
app.configmap.BTG_API_BASE_URL BTG API base URL.
app.configmap.BTG_AUTH_URL BTG OAuth2 token URL.
app.configmap.MIDAZ_LEDGER_URL Midaz Ledger service URL: one URL serves onboarding and transaction. Must be https:// under ENV_NAME=production (the default). The former MIDAZ_ONBOARDING_URL / MIDAZ_TRANSACTION_URL pair is refused at render with the rename named.
app.configmap.ORGANIZATION_IDS "global" or the comma-separated organization UUIDs the license covers. The app validates its license at startup in every environment.
app.secrets.LICENSE_KEY License key. Required in every environment unless app.licenseFile.existingSecret is set. The control-plane migrations Job receives it too.
app.licenseFile.existingSecret Optional Secret holding a signed licence file under the key license, mounted read-only as the directory /etc/lerian/license on the app and the control-plane migrations Job; sets LICENSE_FILE_PATH. Replaces LICENSE_KEY. The Secret must exist before the sync: the PreSync migrations Job mounts it. File mode needs an image on lib-license-go v5.
app.secrets.PLUGIN_AUTH_CLIENT_ID / PLUGIN_AUTH_CLIENT_SECRET Access Manager client credentials of the Midaz client. Single-tenant, while MIDAZ_AUTH_ENABLED is "true" (the default).
app.secrets.PROVIDER_CLIENT_ID Provider OAuth2 client ID. Single-tenant only — in multi-tenant it is resolved per tenant and must be left empty. Named for the role rather than the vendor; renamed from BTG_CLIENT_ID.
app.secrets.PROVIDER_CLIENT_SECRET Provider OAuth2 client secret. Same conditionality as above; renamed from BTG_CLIENT_SECRET.
app.secrets.INTERNAL_API_KEY At least 32 characters. Required when SERVICE_TYPE includes worker (default both). Generate with openssl rand -hex 32.
app.secrets.CREDENTIAL_ENCRYPTION_KEY Base64-encoded AES-256 key. Required when SERVICE_TYPE includes worker, when multi-tenant serves the API, or when ENV_NAME=production (the default). Generate with openssl rand -base64 32.
app.secrets.BTG_WEBHOOK_HMAC_SECRET HMAC-SHA256 secret of the provider webhook subscription. Required wherever the API is served (SERVICE_TYPE both, the default, or api) and on every pod when ENV_NAME=production (the default), in every tenancy mode; at least 32 characters whenever set. Generate with openssl rand -hex 32.
app.secrets.CONSULT_AUDIT_HMAC_KEY HMAC key of the bill consult audit’s line fingerprint (line_fp). Required when SERVICE_TYPE serves the API (both, the default, or api); the app refuses to boot without it. At least 32 characters, no leading or trailing whitespace; with ENV_NAME=production the app also refuses the published example placeholders. Generate with openssl rand -hex 32 and, outside local, source it from the platform secret manager. Rotating it is a restart, but older line_fp values stop correlating (there is no _PREVIOUS key). Not checked at render when app.useExistingSecret=true.

Secret keys are not checked at render with app.useExistingSecret=true; the Secret named by app.existingSecretName must carry them.

When app.configmap.MULTI_TENANT_ENABLED=true, the PROVIDER_CLIENT_* and PLUGIN_AUTH_CLIENT_* pairs above stop being required — the app resolves them per tenant — and the following are additionally required, with an external Postgres (postgresql.enabled=false) so the control-plane migrations Job can run:

Field Description
app.configmap.MULTI_TENANT_URL Tenant Manager service URL (or env-wide global.multiTenant.url).
app.secrets.MULTI_TENANT_SERVICE_API_KEY Tenant Manager service API key.
app.configmap.MULTI_TENANT_CREDENTIAL_SOURCE Must be "vault" — the only accepted value. The app fails closed at boot for any other value (empty, a typo, or the retired "tenant_manager" spelling).
app.configmap.AWS_REGION Standard AWS SDK region variable, read directly by the AWS SDK when building the Secrets Manager client for the per-tenant integrations bundle.
app.configmap.WEBHOOK_PUBLIC_BASE_URL Public base URL the connect flow composes each tenant’s webhook URL from. Required where the API is served.

Database password: with the bundled PostgreSQL subchart (default), the password is auto-generated into the subchart’s own Secret and read by the app via secretKeyRef — leave app.secrets.POSTGRES_PASSWORD empty. Only set it for an external Postgres that has no postgresql.auth.existingSecret.

Upgrading to the template-owned configuration

The ConfigMap and Secret no longer echo every key of app.configmap / app.secrets: they render the application contract listed in Configuration keys, and the render fails on a key the application renamed or stopped reading, naming the fix. Before upgrading, in your overlay:

  1. Rename BTG_CLIENT_ID / BTG_CLIENT_SECRET to PROVIDER_CLIENT_ID / PROVIDER_CLIENT_SECRET, BTG_WEBHOOK_SECRET to BTG_WEBHOOK_HMAC_SECRET, MIDAZ_ONBOARDING_URL + MIDAZ_TRANSACTION_URL to one MIDAZ_LEDGER_URL, and the deprecated multi-tenant names (MULTI_TENANCY_ENABLED, MULTI_TENANT_MANAGER_URL, MULTI_TENANT_CLIENT_TIMEOUT_SEC, MULTI_TENANT_CACHE_TTL_MINUTES in minutes to MULTI_TENANT_CACHE_TTL_SEC in seconds, MULTI_TENANT_CB_*) to their canonical names.
  2. Remove keys the application no longer reads: OUTBOX_ENABLED, OUTBOX_TABLE_NAME, CIRCUIT_BREAKER_ENABLED, RATE_LIMIT_READ, RATE_LIMIT_WRITE, RECONCILIATION_LOOKBACK_HOURS, RECONCILIATION_MAX_PROVIDER_PAGES, EXAMPLE_STATUS_PROVIDER_MODE.
  3. Drop the null workarounds for OUTBOX_PUBLISH_MAX_ATTEMPTS, OUTBOX_RETRY_WINDOW_SEC, OUTBOX_PROCESSING_TIMEOUT_SEC and RECONCILIATION_INTERVAL: the chart no longer ships defaults for them, so the application applies its own. A null still works (it counts as unset).
  4. Multi-tenant: MULTI_TENANT_CREDENTIAL_SOURCE defaults to "vault", the only value the application accepts; AWS_REGION is required.
  5. Bill consult: set app.secrets.CONSULT_AUDIT_HMAC_KEY (at least 32 characters, openssl rand -hex 32, from the platform secret manager) for SERVICE_TYPE both or api, or add it to the Secret named by app.existingSecretName. Without it the render fails, and the application would refuse to boot.
  6. HTTP edge: TRUSTED_PROXIES is read by the application again (it is no longer refused at render). Set app.configmap.TRUSTED_PROXIES to the CIDRs of the proxies or load balancers in front of the service, comma separated (for example "10.123.0.0/16,10.42.0.0/16"). Only CIDR notation is accepted, and nothing broader than /8 (IPv4) or /48 (IPv6). It is the platform-wide variable: the auth library uses the same value for the client IP it sends to the Access Manager. Unset, the application warns at boot and uses the socket peer. HTTP_RECEIVE_TIMEOUT_SEC and HTTP_IDLE_TIMEOUT_SEC render only when set. The HTTP_BODY_LIMIT_BYTES chart default drops from "104857600" to "4194304" (4 MiB), which is also the application’s ceiling: an overlay that sets a larger value is clamped to 4 MiB and logs a SECURITY: warning on every boot, so remove it or set it at or below 4 MiB.

Optionally move connection endpoints to the global: block (see values-template.yaml); native app.configmap.<KEY> values keep working and win over it. Pods now roll on a ConfigMap or Secret change (checksum/* annotations), so a restartedAt annotation bump is no longer needed.

Configuration keys

The ConfigMap and Secret are template-owned: they render exactly the keys below, and values.schema.json refuses any other key under app.configmap / app.secrets. Set app.configmap.<KEY> to override a key; a key marked unset is rendered only when you set it, so the application default applies otherwise. A null counts as unset. Connection endpoints resolve through the lerian-common masks, native key first: app.configmap.<KEY> > datastores.* > global.datastores.* > global.cloud preset > chart default. app.extraEnvVars stays a verbatim escape hatch.

APPLICATION | Key | Default | |-----|---------| | `ENV_NAME` | `global.env.name`, else `"production"` | | `LOG_LEVEL` | `"info"` | | `DEPLOYMENT_MODE` | `"byoc"` | | `SERVICE_TYPE` | `"both"` | | `SWAGGER_ENABLED` | `"false"` | | `VERSION` | the image tag | | `ALLOW_INSECURE_TLS` | `"true"` |
HTTP SERVER | Key | Default | |-----|---------| | `SERVER_ADDRESS` | `"0.0.0.0:8080"` | | `HTTP_BODY_LIMIT_BYTES` | `"4194304"` (4 MiB, also the application's ceiling: a larger value is clamped to 4 MiB and logs a `SECURITY:` warning on every boot) | | `HTTP_READ_BUFFER_SIZE` | unset (app default) | | `HTTP_RECEIVE_TIMEOUT_SEC` | unset (app default `30`) | | `HTTP_IDLE_TIMEOUT_SEC` | unset (app default `120`) | | `TRUSTED_PROXIES` | unset (app default: no proxy is trusted, `X-Forwarded-For` is ignored and the socket peer is used, with a warning at boot) | | `INTERNAL_WORKER_URL` | `""` |
CORS | Key | Default | |-----|---------| | `ACCESS_CONTROL_ALLOW_ORIGIN` | `""` (deny every browser origin) | | `ACCESS_CONTROL_ALLOW_METHODS` | `"GET,POST,PUT,PATCH,DELETE,OPTIONS"` | | `ACCESS_CONTROL_ALLOW_HEADERS` | `"Origin,Content-Type,Accept,Authorization,X-Request-ID"` | | `ACCESS_CONTROL_EXPOSE_HEADERS` | `""` | | `ACCESS_CONTROL_ALLOW_CREDENTIALS` | `"false"` |
RATE LIMITING | Key | Default | |-----|---------| | `RATE_LIMIT_ENABLED` | `"true"` | | `RATE_LIMIT_MAX` | `"500"` | | `RATE_LIMIT_WINDOW_SEC` | `"60"` | | `AGGRESSIVE_RATE_LIMIT_MAX` | `"100"` | | `AGGRESSIVE_RATE_LIMIT_WINDOW_SEC` | `"60"` | | `RELAXED_RATE_LIMIT_MAX` | `"1000"` | | `RELAXED_RATE_LIMIT_WINDOW_SEC` | `"60"` | | `CONSULT_RATE_LIMIT_MAX` | unset (app default `100`) | | `CONSULT_RATE_LIMIT_WINDOW_SEC` | unset (app default `60`) | `CONSULT_RATE_LIMIT_*` is the bill consult route's own in-process limit per (tenantId, sub). It is not the `AGGRESSIVE_*` tier, and `RATE_LIMIT_ENABLED` does not turn it off.
MULTI-TENANCY | Key | Default | |-----|---------| | `MULTI_TENANT_ENABLED` | `global.multiTenant.enabled`, else unset | | `MULTI_TENANT_URL` | `global.multiTenant.url`, else `""` | | `MULTI_TENANT_SERVICE_NAME` | `"plugin-br-payments"` | | `MULTI_TENANT_POSTGRES_MODULE` | `""` | | `MULTI_TENANT_ALLOW_INSECURE_HTTP` | `"false"` | | `MULTI_TENANT_TIMEOUT` | `"10"` | | `MULTI_TENANT_CACHE_TTL_SEC` | `"3600"` | | `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` | `"5"` | | `MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC` | `"30"` | | `MULTI_TENANT_MAX_TENANT_POOLS` | `"100"` | | `MULTI_TENANT_CREDENTIAL_SOURCE` | `"vault"` | | `MULTI_TENANT_DISCOVERY_INTERVAL_SEC` | unset (app default) | | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | unset (app default) | | `AWS_REGION` | unset (app default) |
POSTGRESQL | Key | Default | |-----|---------| | `POSTGRES_HOST` | `datastores` / `global.datastores` `postgres.host`, else the bundled subchart | | `POSTGRES_PORT` | `postgres.port` mask, else `"5432"` | | `POSTGRES_USER` | `postgres.user` mask, else `"plugin_br_payments"` | | `POSTGRES_DB` | `postgres.name` mask, else `"plugin_br_payments"` | | `POSTGRES_SSLMODE` | `postgres.ssl` mask, else `"require"` | | `POSTGRES_MAX_IDLE_CONNS` | `"15"` | | `POSTGRES_MAX_OPEN_CONNS` | `"25"` | | `POSTGRES_CONN_MAX_LIFETIME_MINS` | `"30"` | | `POSTGRES_CONN_MAX_IDLE_TIME_MINS` | `"5"` | | `POSTGRES_CONNECT_TIMEOUT_SEC` | `"10"` | | `POSTGRES_REPLICA_HOST` | `postgres.replicaHost` mask, else unset | | `POSTGRES_REPLICA_PORT` | unset (app default) | | `POSTGRES_REPLICA_USER` | unset (app default) | | `POSTGRES_REPLICA_DB` | unset (app default) | | `POSTGRES_REPLICA_SSLMODE` | unset (app default) | | `INFRA_CONNECT_TIMEOUT_SEC` | `"30"` | | `MIGRATION_TIMEOUT_SEC` | `"300"` | | `MIGRATION_LOCK_TIMEOUT_MS` | `"10000"` | | `DB_METRICS_INTERVAL_SEC` | `"15"` | | `IDEMPOTENCY_RECORD_TTL_HOURS` | `"48"` |
OUTBOX | Key | Default | |-----|---------| | `OUTBOX_DISPATCH_INTERVAL_SEC` | `"2"` | | `OUTBOX_BATCH_SIZE` | `"50"` | | `OUTBOX_PUBLISH_BACKOFF_MS` | `"200"` | | `OUTBOX_MAX_DISPATCH_ATTEMPTS` | `"10"` | | `OUTBOX_MAX_FAILED_PER_BATCH` | `"25"` | | `OUTBOX_INCLUDE_TENANT_METRICS` | `"false"` | | `OUTBOX_ALLOW_EMPTY_TENANT` | `"true"` | | `OUTBOX_PRIORITY_EVENT_TYPES` | unset (app default) | | `OUTBOX_PUBLISH_MAX_ATTEMPTS` | unset (app default) | | `OUTBOX_RETRY_WINDOW_SEC` | unset (app default) | | `OUTBOX_PROCESSING_TIMEOUT_SEC` | unset (app default) |
AUTHENTICATION (plugin-access-manager) | Key | Default | |-----|---------| | `PLUGIN_AUTH_ENABLED` | `global.auth.enabled`, else `"true"` | | `PLUGIN_AUTH_ADDRESS` | `global.auth.host`, else the in-cluster plugin-access-manager | | `PLUGIN_AUTH_HOST` | unset (lib-auth falls back to `PLUGIN_AUTH_ADDRESS`, with a warning) | | `AUTH_M2M_INVERSION_ENABLED` | unset (lib-auth default `false`) | | `AUTH_M2M_PRODUCT_FORWARD_ENABLED` | unset (lib-auth default `false`); with the key above, M2M callers are authorized on the responsibility-inversion (RI) path | | `IDP_DECLARATION_ENABLED` | unset (off). When `"true"`, the app publishes its permission manifest to identity at boot and refuses to boot without `IDP_HOST` and the `IDP_M2M_CLIENT_ID` / `IDP_M2M_CLIENT_SECRET` secrets | | `IDP_HOST` | unset; the identity URL the manifest is published to |
PROVIDER (BTG) | Key | Default | |-----|---------| | `BTG_API_BASE_URL` | `""` | | `BTG_AUTH_URL` | `""` | | `BTG_COMMON_BASE_URL` | `""` | | `BTG_TOKEN_REFRESH_INTERVAL` | `"4m"` | | `BTG_HTTP_TIMEOUT` | unset (app default) | | `REQUEST_TIMEOUT_SEC` | unset (app default; app 1.2.0 and later; must be at least four `BTG_HTTP_TIMEOUT`s) | | `BTG_CREDENTIAL_SYNC_INTERVAL` | unset (app default) | | `PROVIDER_CREDENTIAL_CLAIM_LEASE` | unset (app default) | | `BTG_MTLS_OUTBOUND_ENABLED` | unset (app default) | | `BTG_MTLS_OUTBOUND_CERT_FILE` | unset (app default) | | `BTG_MTLS_OUTBOUND_KEY_FILE` | unset (app default) | | `BTG_MTLS_OUTBOUND_CA_FILE` | unset (app default) | | `BTG_MTLS_INBOUND_ENABLED` | unset (app default) | | `BTG_MTLS_INBOUND_CERT_HEADER` | unset (app default) | | `BTG_WEBHOOK_CERTIFICATE_URL` | unset (app default) | | `BTG_BULK_MAX_BODY_BYTES` | read only by app versions after 1.0.2; unset (app default `7680` bytes; zero, negative or non-numeric stops the boot) | | `SANDBOX_SIMULATION_ENABLED` | unset (app default `"false"`; `"true"` enables `POST /v1/sandbox/boletos/{id}/simulate-payment` and `POST /v1/sandbox/payments/{id}/simulate-status`; with it on, the app refuses to boot when `ENV_NAME=production` and requires `BTG_API_BASE_URL`, `BTG_AUTH_URL`, `BTG_COMMON_BASE_URL` and `SANDBOX_FAKEBTG_ADMIN_URL` to share one origin, `scheme://host:port`) | | `SANDBOX_FAKEBTG_ADMIN_URL` | unset (no app default; required when `SANDBOX_SIMULATION_ENABLED` is `"true"`) |
BOLETO BATCH | Key | Default | |-----|---------| | `BOLETO_BATCH_INLINE_SUBMIT_BUDGET` | read only by app versions after 1.0.2; unset (app default `8s`; bounds only the request's wait, never the provider call) | | `BOLETO_BATCH_UNKNOWN_ATTEMPTS_BEFORE_SEARCH` | read only by app versions after 1.0.2; unset (app default `3`; non-numeric stops the boot) | | `BOLETO_BATCH_ITEM_OVERDUE_AFTER` | read only by app versions after 1.0.2; unset (app default `30m`; an overdue item is reported, never failed) | | `BOLETO_BATCH_CANCEL_CONCURRENCY` | read only by app versions after 1.0.2; unset (app default `5`; non-numeric stops the boot) |
MIDAZ | Key | Default | |-----|---------| | `MIDAZ_LEDGER_URL` | `""` | | `MIDAZ_CRM_URL` | retired by app 1.0.2 (it only logs a warning); still rendered when set | | `MIDAZ_AUTH_ENABLED` | `"true"` (the app refuses `false` under `ENV_NAME=production`) | | `MIDAZ_ALLOW_INSECURE_HTTP` | `"true"` (lets the Midaz client reach an `http://` `PLUGIN_AUTH_ADDRESS`; `MIDAZ_LEDGER_URL` stays HTTPS-only under production) | | `MIDAZ_DEFAULT_ORG_ID` | retired by app 1.0.2 (it only logs a warning); still rendered when set | | `MIDAZ_DEFAULT_LEDGER_ID` | retired by app 1.0.2 (it only logs a warning); still rendered when set | | `M2M_CRM_TARGET_SERVICE` | retired by app 1.0.2 (it only logs a warning); still rendered when set | | `ORGANIZATION_IDS` | unset — **required** (`"global"` or organization UUIDs) | | `ACCOUNT_VALIDATION_DISABLED` | unset (app default) | | `ACCOUNTING_ROUTES_ENABLED` | unset (app default) | | `ACCOUNTING_ROUTES_CACHE_TTL_SEC` | unset (app default) | | `ACCOUNTING_ROUTES_FETCH_TIMEOUT_SEC` | unset (app default) |
RECONCILIATION | Key | Default | |-----|---------| | `RECONCILIATION_INTERVAL` | unset (app default) | | `RECONCILIATION_DELAY_MS` | unset (app default) | | `RECONCILIATION_MAX_BOLETOS_PER_CYCLE` | unset (app default) | | `RECONCILIATION_MAX_MONEY_ITEMS_PER_CYCLE` | unset (app default) | | `RECONCILIATION_BOLETO_GRACE_PERIOD` | unset (app default) | | `RECONCILIATION_BOLETO_PROCESSING_CEILING` | unset (app default `24h`; must exceed `RECONCILIATION_BOLETO_GRACE_PERIOD`, or the worker refuses to start) | | `RECONCILIATION_BOLETO_RESERVATION_GRACE_PERIOD` | unset (app default `30m`; clamped to at least `15m` and to four `BTG_HTTP_TIMEOUT`s, no ceiling) | | `RECONCILIATION_PAYMENT_GRACE_PERIOD` | unset (app default) | | `PAYMENT_UNCONFIRMED_DISPATCH_GRACE` | unset (app default `5m`) | | `PAYMENT_UNCONFIRMED_ABSENCE_WINDOW` | unset (app default `30m`) | | `PAYMENT_UNCONFIRMED_BATCH_SIZE` | unset (app default `50`) | | `RECONCILIATION_WEBHOOK_CLAIM_LEASE` | unset (app default) | | `RECONCILIATION_WEBHOOK_SILENCE_WINDOW` | unset (app default) | | `RECONCILIATION_BATCH_SUBMISSION_CLAIM_LEASE` | read only by app versions after 1.0.2; unset (app default `3m`; floored at `4 × BTG_HTTP_TIMEOUT + 1m`) | | `RECONCILIATION_BATCH_SUBMISSIONS_PER_CYCLE` | read only by app versions after 1.0.2; unset (app default `10`; non-numeric stops the boot) | | `RECONCILIATION_BATCH_RECOVERY_ALERT_AGE` | read only by app versions after 1.0.2; unset (app default `1h`; the alert never changes the item) | | `RECONCILIATION_INSTALLMENT_ACCOUNT_DISCOVERY_ALERT_AGE` | read only by app versions after 1.0.2; unset (app default `1h`; the alert never changes the installment) | | `RECONCILIATION_INSTALLMENT_CLAIM_LEASE` | read only by app versions after 1.0.2; unset (app default `10m`; floored at `4 × BTG_HTTP_TIMEOUT + 1m`) | | `RECONCILIATION_INSTALLMENT_DISCOUNT_CUTOFF_MARGIN` | read only by app versions after 1.0.2; unset (app default `30m`; capped at `6h`; zero, negative or unparseable takes the default) | | `RECONCILIATION_INSTALLMENT_RECOVERY_ALERT_AGE` | read only by app versions after 1.0.2; unset (app default `1h`; the alert never changes the installment) | | `RECONCILIATION_INSTALLMENT_RECOVERY_ESCALATION_AGE` | read only by app versions after 1.0.2; unset (app default `24h`; an escalation not greater than the alert age is replaced at boot, with a warning) |
WEBHOOKS | Key | Default | |-----|---------| | `WEBHOOK_PUBLIC_BASE_URL` | unset (app default) | | `WEBHOOK_SKIP_URL_VALIDATION` | unset (app default) | | `WEBHOOK_CONNECTION_GRACE_WINDOW_SEC` | unset (app default) | | `WEBHOOK_DISPATCHER_INTERVAL` | unset (app default) | | `WEBHOOK_DISPATCHER_BATCH_SIZE` | unset (app default) | | `WEBHOOK_DISPATCHER_CLAIM_LEASE` | unset (app default) | | `WEBHOOK_CONSUMER_INTERVAL` | unset (app default) |
TELEMETRY | Key | Default | |-----|---------| | `ENABLE_TELEMETRY` | `global.observability.enabled`, else `"true"` when an OTLP endpoint is configured or `otel-collector-lerian.enabled=true`, else `"false"` | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `global.observability.otlpEndpoint`, else `""` | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | `global.observability.deploymentEnvironment`, else `"production"` | | `OTEL_LIBRARY_NAME` | `"github.com/LerianStudio/plugin-br-payments"` | | `OTEL_RESOURCE_SERVICE_NAME` | `"plugin-br-payments"` | | `OTEL_RESOURCE_SERVICE_VERSION` | the image tag | The chart injects `ALLOW_INSECURE_OTEL` only together with the node-local collector endpoint it sets itself (`otel-collector-lerian.enabled=true`, plaintext `$(HOST_IP):4317`); a plaintext endpoint you supply gets no automatic opt-out, so while `deploymentEnvironment` is `production` or `prod` (the chart default) either use `https://` or set `ALLOW_INSECURE_OTEL` yourself through `app.extraEnvVars`.

Secret keys (app.secrets, each rendered only when set): POSTGRES_PASSWORD, POSTGRES_REPLICA_PASSWORD, PROVIDER_CLIENT_ID, PROVIDER_CLIENT_SECRET, BTG_WEBHOOK_HMAC_SECRET, INTERNAL_API_KEY, INTERNAL_API_KEY_PREVIOUS, CREDENTIAL_ENCRYPTION_KEY, CREDENTIAL_ENCRYPTION_KEY_PREVIOUS, LICENSE_KEY, PLUGIN_AUTH_CLIENT_ID, PLUGIN_AUTH_CLIENT_SECRET, MULTI_TENANT_SERVICE_API_KEY, CONSULT_AUDIT_HMAC_KEY, IDP_M2M_CLIENT_ID, IDP_M2M_CLIENT_SECRET.

Refused at render, with the replacement named: MULTI_TENANCY_ENABLED, MULTI_TENANT_MANAGER_URL, MULTI_TENANT_CLIENT_TIMEOUT_SEC, MULTI_TENANT_CACHE_TTL_MINUTES, MULTI_TENANT_CB_THRESHOLD, MULTI_TENANT_CB_TIMEOUT_SEC, MIDAZ_ONBOARDING_URL, MIDAZ_TRANSACTION_URL, OUTBOX_ENABLED, OUTBOX_TABLE_NAME, CIRCUIT_BREAKER_ENABLED, RATE_LIMIT_READ, RATE_LIMIT_WRITE, RECONCILIATION_LOOKBACK_HOURS, EXAMPLE_STATUS_PROVIDER_MODE, RECONCILIATION_MAX_PROVIDER_PAGES (app.configmap); BTG_CLIENT_ID, BTG_CLIENT_SECRET, BTG_WEBHOOK_SECRET (app.secrets). The retired ones are no longer read by the application.

Also refused at render (app.configmap): SERVER_TLS_CERT_FILE, SERVER_TLS_KEY_FILE. The main port serves plain HTTP; terminate TLS at the Ingress or mesh. TLS_TERMINATED_UPSTREAM is no longer rendered: the app stopped reading it.

Probes

The lerian-common.health preset owns probes, ports, grace period, rollout strategy and PodDisruptionBudget; none is a value (the render refuses readinessProbe, livenessProbe, startupProbe, terminationGracePeriodSeconds, HEALTH_PORT and SHUTDOWN_* overrides). The app serves them on the internal port 8099 (HEALTH_PORT), never through the Ingress.

Probe Path (port 8099) Notes
Startup /health Covers boot, including the migrations that now run behind readiness.
Liveness /health Process only.
Readiness /readyz The pod’s own state: Postgres at boot, schema, boot credential, Tenant Manager. The provider and Midaz are not checks, and a later Postgres outage never pulls the pod.
Tenant readiness /readyz/tenant/:id Multi-tenant only, no JWT. Exposed on the Service as the internal health port.

app.health.shutdownSeconds (default 35) is SHUTDOWN_TIMEOUT, one total budget; the grace period is drain (15s) + that + 10s. The preset refuses an image below minImage, the first one that serves the health port.

Multi-tenancy

The plugin supports schema-per-tenant via Lerian’s Tenant Manager. To enable:

postgresql:
  enabled: false
app:
  controlplaneMigrations:
    enabled: true
  configmap:
    POSTGRES_HOST: "<external postgres host>"
    MULTI_TENANT_ENABLED: "true"
    MULTI_TENANT_URL: "https://tenant-manager.example.com"
    MULTI_TENANT_SERVICE_NAME: "plugin-br-payments"
    MULTI_TENANT_CREDENTIAL_SOURCE: "vault"
    AWS_REGION: "<aws-region>"
    WEBHOOK_PUBLIC_BASE_URL: "https://payments.example.com"
  secrets:
    MULTI_TENANT_SERVICE_API_KEY: "<api key>"

When enabled, /readyz/tenant/:id becomes available and the provider credential is read per tenant, so use the per-tenant probe for it. Do not set app.secrets.PROVIDER_CLIENT_ID or app.secrets.PROVIDER_CLIENT_SECRET; the application resolves those credentials per tenant. The control-plane migrations Job (app.controlplaneMigrations.enabled, required for multi-tenant) runs before each install and upgrade; the render fails if it cannot (bundled Postgres, or the Job disabled).

AWS credentials for Secrets Manager (aws.rolesAnywhere)

Multi-tenant mode with MULTI_TENANT_CREDENTIAL_SOURCE: "vault" calls AWS Secrets Manager to read each tenant’s integrations bundle. On a cluster that already runs on AWS (IRSA), annotate the ServiceAccount with the IAM role through app.serviceAccount.annotations. On a non-AWS cluster (on-prem, Proxmox, another cloud) there is no such mechanism, so enable IAM Roles Anywhere: an aws-signing-helper sidecar exchanges an X.509 client certificate for temporary AWS credentials and serves them on a local metadata endpoint the app reads via the standard AWS SDK credential chain.

aws:
  rolesAnywhere:
    enabled: true
    trustAnchorArn: "arn:aws:rolesanywhere:<region>:<account>:trust-anchor/<id>"
    profileArn: "arn:aws:rolesanywhere:<region>:<account>:profile/<id>"
    roleArn: "arn:aws:iam::<account>:role/<role>"
    region: "<region>"  # MUST match the region trustAnchorArn/profileArn were created in — Roles Anywhere resources are regional; defaults to us-east-2
    certificateSecretName: "plugin-br-payments-iam-tls"  # cert-manager Secret the sidecar mounts

trustAnchorArn, profileArn, and roleArn are required once enabled: true — the render fails closed otherwise. The certificateSecretName Secret (containing tls.crt/tls.key) is provisioned by the GitOps deploy layer as a cert-manager Certificate, not by this chart; see the Roles Anywhere deploy docs for that half. Off by default — zero cost when unset.

Accounting routes

The accounting-route resolver attaches a routeId to the plugin’s Midaz transactions by resolving the tenant’s operation and transaction routes. The chart renders these keys only when set under app.configmap: the app owns their defaults, and omitted, the resolver stays off, which is the behaviour the plugin had before the feature existed.

Key App default Meaning
ACCOUNTING_ROUTES_ENABLED false Gates the resolver as a whole.
ACCOUNTING_ROUTES_CACHE_TTL_SEC 900 Per-tenant route inventory cache TTL, in seconds; <= 0 disables the cache.
ACCOUNTING_ROUTES_FETCH_TIMEOUT_SEC 60 Budget for one full route resolution, in seconds; <= 0 falls back to the default.
RECONCILIATION_WEBHOOK_CLAIM_LEASE 2h How long one replica’s claim on a webhook row stays exclusive.

Turn it on per environment, one deployment at a time, after the per-tenant credentials exist — a tenant whose credential is not provisioned yet is taken down, not degraded, by the first request that reaches the resolver:

app:
  configmap:
    ACCOUNTING_ROUTES_ENABLED: "true"

If you also disable the routes cache (ACCOUNTING_ROUTES_CACHE_TTL_SEC <= 0), raise RECONCILIATION_WEBHOOK_CLAIM_LEASE first, to at least RECONCILIATION_MAX_MONEY_ITEMS_PER_CYCLE x (65s + ACCOUNTING_ROUTES_FETCH_TIMEOUT_SEC) — 3h30m at the defaults. The lease is a fencing token’s lifetime, not a timeout: shorter than the batch it covers, it lets a second replica take rows the first is still working. The full rollout sequence is the plugin’s accounting-routes runbook.

Common values

Key Default Description
app.replicaCount 2 Number of replicas.
app.image.repository ghcr.io/lerianstudio/plugin-br-payments Container image.
app.image.tag "" (Chart appVersion) Image tag.
app.service.port 8080 Service port.
app.ingress.enabled false Expose via Ingress.
app.autoscaling.enabled true Enable HPA.
app.health.shutdownSeconds 35 SHUTDOWN_TIMEOUT; the grace period is derived (drain 15s + this + 10s).
app.configmap.SERVICE_TYPE both Run API + worker in one process.
app.configmap.MULTI_TENANT_ENABLED unset (app default false) Toggle multi-tenant mode; env-wide via global.multiTenant.enabled. The deprecated MULTI_TENANCY_ENABLED is refused at render with the rename named.
app.controlplaneMigrations.enabled false Enable the control-plane migrations PreSync Job. Renders with MULTI_TENANT_ENABLED=true and the bundled PostgreSQL disabled; it does not require database bootstrap.
app.controlplaneMigrations.resources requests 50m/64Mi, limits 250m/256Mi Resources for the control-plane migrations Job.
postgresql.enabled true Deploy the in-cluster PostgreSQL subchart.
postgresql.architecture replication Primary + read replica.
global.externalPostgresDefinitions.enabled false Run a bootstrap Job against an external PostgreSQL.
otel-collector-lerian.enabled false Inject host-level OTLP endpoint env vars.
aws.rolesAnywhere.enabled false Enable the aws-signing-helper sidecar for AWS credentials on non-AWS clusters.

See values.yaml for the full list, and values-template.yaml for a production-ready overlay starter.

Production layout

In production, you typically:

  1. Disable the in-cluster PostgreSQL and point at a managed service. app.configmap.POSTGRES_HOST/POSTGRES_PORT is the single source the app Deployment, the control-plane migrations Job, and the bootstrap Job all read to reach Postgres — set it to your real external host/port. global.externalPostgresDefinitions.connection.host/.port is no longer read by any template (kept only for backward compatibility with older overlays) — you do not need to set it, and setting it to a different value than app.configmap.POSTGRES_HOST/.PORT has no effect on where the bootstrap Job connects:
    postgresql:
      enabled: false
    
    app:
      configmap:
        POSTGRES_HOST: my-rds-instance.example.com
        POSTGRES_PORT: "5432"
        POSTGRES_SSLMODE: require
    
  2. Use existing secrets instead of inline values:
    app:
      useExistingSecret: true
      existingSecretName: plugin-br-payments-secrets
    
  3. Optional bootstrap for a fresh external Postgres (creates DB + role + grants, idempotent). It connects to the same app.configmap.POSTGRES_HOST/POSTGRES_PORT set in step 1 above — only the admin credentials it needs to create the role and database are configured here:
    global:
      externalPostgresDefinitions:
        enabled: true
        postgresAdminLogin:
          username: postgres
          password: <admin password>
        paymentsCredentials:
          password: <plugin_br_payments role password>
    
  4. Enable control-plane migrations for multi-tenant deployments after the external database already exists:
    app:
      controlplaneMigrations:
        enabled: true
    

    This runs /controlplane-migrate as a PreSync/pre-upgrade Job against app.configmap.POSTGRES_*. It is independent of global.externalPostgresDefinitions.enabled; keep the bootstrap flag off when the database and role are already provisioned.

Uninstall

helm uninstall my-payments -n midaz-plugins

If postgresql.enabled=true was used, the PVCs are NOT deleted automatically. Remove them manually if no longer needed:

kubectl delete pvc -n midaz-plugins -l app.kubernetes.io/instance=my-payments

License

Elastic License 2.0 (chart). The plugin-br-payments application itself is proprietary to Lerian Studio Ltd.