single-serviceapp.secrets.LICENSE_KEY in every environment (unless app.licenseFile.existingSecret mounts a signed licence file); PROVIDER_CLIENT_ID, PROVIDER_CLIENT_SECRET, INTERNAL_API_KEY, CREDENTIAL_ENCRYPTION_KEY and BTG_WEBHOOK_HMAC_SECRET (at least 32 characters) for the default production worker-enabled render; CONSULT_AUDIT_HMAC_KEY wherever the API is served (SERVICE_TYPE both, the default, or api); and PLUGIN_AUTH_CLIENT_ID / PLUGIN_AUTH_CLIENT_SECRET in single-tenant while MIDAZ_AUTH_ENABLED is "true" (the default). The PROVIDER_CLIENT_* pair is required in single-tenant only; in multi-tenant it is resolved per tenant and must be left empty. With the bundled PostgreSQL subchart the database password is auto-generated and read via secretKeyRef — only supply POSTGRES_PASSWORD for an external Postgres without postgresql.auth.existingSecret.github.com/LerianStudio/helm; license is Elastic License 2.0.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.
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.
postgresql.enabled=true), orplugin-auth (lib-auth) deployment when PLUGIN_AUTH_ENABLED=true.ghcr.io/lerianstudio/plugin-br-payments, which is not public: list a registry Secret in app.imagePullSecrets (the control-plane migrations Job uses the same list).The chart deploys one Deployment, one process (/app with SERVICE_TYPE=both):
RECONCILIATION_INTERVAL)OUTBOX_DISPATCH_INTERVAL_SEC)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.
The plugin uses PostgreSQL only:
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— leaveapp.secrets.POSTGRES_PASSWORDempty. Only set it for an external Postgres that has nopostgresql.auth.existingSecret.
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:
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.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.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).MULTI_TENANT_CREDENTIAL_SOURCE defaults to "vault", the only value
the application accepts; AWS_REGION is required.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.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.
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.
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.
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.
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.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.
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.
| 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.
In production, you typically:
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
app:
useExistingSecret: true
existingSecretName: plugin-br-payments-secrets
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>
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.
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
Elastic License 2.0 (chart). The plugin-br-payments application itself is proprietary to Lerian Studio Ltd.