Self-hosted & ConnectorsSelf-Hosted Overview

Self-Hosted Overview

Run transmute in your own infrastructure under an offline licence: what the licence controls, which image to pick, a minimal Helm install and every environment variable a self-hosted deployment reads.

What a self-hosted install is

A self-hosted (on-premises) install runs the same API you call at api.transmute.403fin.io, inside your own network. It consists of three things:

PieceWhat it isWhere it comes from
Container imageOne image carrying four binaries: /api (the default entrypoint), /worker, /connector and /adminghcr.io/403ent/transmute:<release>, or a docker save tarball for air-gapped sites
Helm chart (optional)A Kubernetes chart released alongside each image tagDelivered with your release
Licence fileA signed token naming your organisation, plan, enabled features and expiryIssued to you by 403 Finance, delivered separately

You also provide a PostgreSQL 16+ database. It holds metadata only (tenants, API key hashes, usage counters, batch job state), never message bodies. The API applies its own schema migrations at startup; there is no separate migration step. Redis is optional and only needed when you run more than one API replica (Scale out).

Always pin a release tag. Never deploy latest.

Licence mode

Setting TRANSMUTE_LICENSE switches the server into licence mode. The licence is an Ed25519-signed token, verified at startup against a public key built into the release image. Verification is entirely offline: the server makes no network call to check it, and no binary in the image reports usage, phones home or checks for updates. Air-gapped installs are fully supported.

TRANSMUTE_LICENSE accepts either the token itself or, when the value starts with /, the path to a file containing it. Mount the file read-only and point the variable at it; the token string is never logged (only its id, organisation, plan, features and expiry are).

What licence mode changes compared with the hosted service:

AreaBehaviour under a licence
API keys, scopes, rate limitsUnchanged. Every request still needs a key; you create them with /admin
Monthly quotaUnlimited. X-Quota-Remaining never counts down
BillingNone. No payment provider is contacted and no billing routes exist
Usage recordsStill written to your own Postgres, metadata only, for your visibility
Optional featuresEnabled by the licence's feature list (below)

Expiry

WhenBehaviour
More than 30 days before expiryNormal operation
Within 30 days of expiryRuns normally; logs one warning per day
Expired, inside the grace windowStill runs; logs one error per day with the grace end date
Expired beyond graceRefuses to start

The check runs at startup. A process that crosses the grace end while running keeps serving until its next restart, then refuses. Renew before expiry: replace the licence file and restart. A malformed or tampered licence also stops the server at startup; it never falls back to unlicensed behaviour. There is no remote revocation, so treat the licence file as confidential.

Feature gates

Core conversion, normalization, validation (syntax, schema) and GET /v1/formats are never gated. A licence issued without a feature list enables everything; a licence with a list enables exactly the listed features:

FeatureUnlocks
batchPOST /v1/convert/batch, /v1/jobs/* and the job worker
webhooks/v1/webhooks
enrichPOST /v1/enrich
cbpr, hvps, fedwire, sepa, cgi, networkThe matching level= value on POST /v1/validate
connections/v1/connections and batch delivery to a connection
transactionsPosting accounting transactions through a connection (requires connections)
overridesMapping overrides, including /v1/overrides and TRANSMUTE_MAPPING_OVERRIDES

404 versus 503

The two status codes tell you different things, so you can tell a licensing gap from a configuration gap:

  • Not licensed → 404. A feature that is not in your licence does not exist in the deployment: its routes are never mounted and answer not-found. An unlicensed validation level is rejected like any unknown level, with invalid-input 400, and GET /v1/formats lists only the levels your deployment serves.
  • Licensed but misconfigured → 503. A feature you are licensed for that is missing its configuration answers 503. For example, batch without TRANSMUTE_JOB_KEY answers batch-unavailable, and connections without a job key answer 503 as well. The synchronous API keeps working.

Images

Every release publishes two images from the same commit:

ImageTagRuntimeXSD validation engine
Default<release>Distroless static, pure Go, no C codestructural
Full XSD<release>-xsdDebian slim with libxml2 (built with cgo)libxml2

The default engine checks well-formedness, required elements, key enumerations, length limits and value patterns. The -xsd flavour validates MX messages against the full ISO 20022 schemas, including element order and cardinality, at the schema level of POST /v1/validate (and at cbpr, which runs schema first). Conversion output is identical on both images; only validation differs. GET /v1/version reports schemaEngine, and validation responses report meta.schemaEngine.

Trade-offs of the -xsd image:

  • It is larger (around 200 MB) and carries libxml2, a C library with its own CVE history. Stay current with releases to receive patched builds. Documents are pre-screened in Go before libxml2 sees them: DTDs are rejected, and network access and external entities are off.
  • Schema validation runs one document at a time per replica. Single validations take milliseconds, but a validation-heavy workload needs more replicas rather than more CPU per pod.

Both images run as uid/gid 65532 with the same entrypoint, port (8080) and binary paths.

Install with Helm

Create the licence and database Secrets, then install with a minimal values file. Secret values never go in the values file: the chart references existing Secrets by name.

kubectl create namespace transmute
kubectl -n transmute create secret generic transmute-license \
  --from-file=license=./your-org.license
kubectl -n transmute create secret generic transmute-db \
  --from-file=DATABASE_URL=./database-url.txt
# values.yaml
image:
  repository: ghcr.io/403ent/transmute
  tag: <release>          # pinned; never latest
  flavor: default         # or xsd for the full-XSD image
license:
  existingSecret: transmute-license
  secretKey: license      # mounted as a file; TRANSMUTE_LICENSE points at it
externalDatabase:
  existingSecret: transmute-db
  secretKey: DATABASE_URL
helm install transmute ./transmute -n transmute -f values.yaml

Then create a tenant and an API key with the admin CLI inside the API pod. The full key is printed once; only its hash is stored.

kubectl -n transmute exec deploy/transmute -- /admin create-tenant --name "Example Org"
kubectl -n transmute exec deploy/transmute -- /admin create-key --tenant <tenant-uuid> --name ops

Other chart values worth knowing: jobKey.existingSecret (enables batch, webhooks and connections), oauth.existingSecret (OAuth client credentials), redis.enabled and replicaCount (Scale out), worker.enabled (Standalone worker), connector.* (File-drop connector), mappingPack.* (Mapping packs), ingress.mtls.* (Mutual TLS), trustedProxies, enrich.* and extraEnv. The chart's liveness and readiness probes call GET /v1/health; GET /v1/ready additionally checks the database.

Docker Compose works the same way: run the image with the variables below in an env_file (mode 600, never committed) and the licence bind-mounted read-only.

Environment variables

Set secrets through the environment (from a Kubernetes Secret or a protected env file), never in config files or chart values. Every variable below is read by the API process.

VarRequiredMeaning
TRANSMUTE_LICENSEYes, for licence modeThe licence token, or a path starting with / to a file containing it
DATABASE_URLYesPostgres DSN. A secret (it carries the password). Unset means an unauthenticated development mode; configured but unreachable stops startup
TRANSMUTE_ENVRecommendedSet to production to make startup fail if DATABASE_URL is missing, instead of serving the unauthenticated development mode
PORTNo (default 8080)Listen port
LOG_LEVELNo (default info)debug for debug logs. Logs never contain message content
TRANSMUTE_JOB_KEYFor batch, webhooks, connections32-byte AES-256 key as 64 hex characters or standard base64. Encrypts batch payloads and connection tokens at rest. Generate with openssl rand -hex 32
TRANSMUTE_JOB_KEY_PREVIOUSNoThe previous job key, kept during a key rotation so existing jobs stay readable
TRANSMUTE_OAUTH_SIGNING_KEYFor OAuth tokensBase64 32-byte Ed25519 seed. Unset leaves /v1/oauth/token and the JWKS route unmounted; API keys still work
TRANSMUTE_EMBEDDED_WORKERNo (default on)false stops the API from running batch jobs itself; see Standalone worker
TRANSMUTE_WORKER_CONCURRENCYNo (default 1)Items the embedded worker processes at once
REDIS_URLFor more than one replicaredis:// or rediss:// URL; see Scale out
TRANSMUTE_MAPPING_PACKNoPath to a signed mapping pack; see Mapping packs
TRANSMUTE_MAPPING_OVERRIDESNoDirectory of override/v1 documents applied to all tenants, loaded at startup (needs the overrides feature). See Mapping overrides
TRANSMUTE_TLS_CERT, TRANSMUTE_TLS_KEY, TRANSMUTE_TLS_CLIENT_CANoServe TLS, and optionally require client certificates; see Mutual TLS
TRANSMUTE_TRUSTED_PROXYNoComma-separated CIDRs of proxies allowed to set forwarding headers. Needed for IP allowlists behind an ingress. Invalid values stop startup
TRANSMUTE_BASE_URLNoPublic base URL, used to build the OAuth redirect URI for accounting connections. Unset derives it from each request's Host
TRANSMUTE_CORS_ORIGINSNo (default off)Comma-separated exact browser origins allowed to call /v1. Unset sends no CORS headers
TRANSMUTE_MAX_BODY_BYTESNo (default 2 MiB)Request body limit
TRANSMUTE_MAX_INFLIGHTNo (default 256)Concurrent conversion/validation requests before excess is shed with 429. 0 or less disables the limit
TRANSMUTE_BIC_DIRECTORY, TRANSMUTE_ENRICH_LEI_MAP, TRANSMUTE_ENRICH_ABA_DIR, TRANSMUTE_ENRICH_BLZ_FILENoReference files you supply for enrichment. A named file that is missing or malformed stops startup
TRANSMUTE_XERO_CLIENT_ID / _SECRET, TRANSMUTE_QBO_CLIENT_ID / _SECRET, TRANSMUTE_SAGE_CLIENT_ID / _SECRETFor accounting connectionsYour OAuth app credentials for each provider. A provider without credentials is not offered
OTEL_EXPORTER_OTLP_ENDPOINTNo (default off)Enables OpenTelemetry tracing over OTLP/HTTP. Spans carry route, method, status and size only. The standard OTEL_EXPORTER_OTLP_* variables and OTEL_SDK_DISABLED are honoured

GET /metrics serves a Prometheus exposition of request metadata (no message content) without authentication, so keep it off any public listener.

Variables used only by the hosted service (billing, the customer portal, its email and bot-check providers and its edge infrastructure) do nothing on a self-hosted install; leave them unset. The vendor-side licence signing key is never set on, or needed by, your systems.

Health endpoints

EndpointAnswers
GET /v1/health200 {"status":"ok"} while the process is up
GET /v1/ready200 when the database answers, 503 {"status":"degraded"} when it does not. Also reports Redis and the loaded mapping pack
GET /v1/versionRelease version, commit and schemaEngine