Enterprise Service Bus (ESB)
The ESB is a forwarding gateway built into the Manager. An external system calls one URL on the Manager with a machine-user token; the Manager checks that the caller may use that endpoint, optionally rewrites the request body with a Go template, adds the backend's own credentials, forwards the request, and records the call in an audit log. Use it when a third party (a webhook sender, a partner system, a legacy SOAP client) needs to reach one of your backends without holding that backend's credentials or knowing its address.
Overview
Callers present a machine-user Bearer token. The application ID and the inbound path select the endpoint. A Go template can rewrite the request body before forwarding (responses are relayed unchanged), and the forwarded request carries Basic, Bearer, or no credentials, as configured on the endpoint. Each call is audited at one of three verbosity levels (NONE, BASIC, FULL).
Quick Start
1. Create an Endpoint
Navigate to your application and create an endpoint:
- Name: Descriptive name (e.g., "Webhook Receiver")
- Consume URL: Path that triggers this endpoint (e.g.,
/api/webhook) - URL: Target backend URL (e.g.,
https://backend.example.com/webhook) - Enabled: Toggle to enable the endpoint
- Auth Type: Authentication method for backend (None, Bearer, Basic)
- Transformation Template: Optional Go template for message transformation
- Log Level: Audit logging verbosity (None, Basic, Full)
The two URL fields are not interchangeable
Consume URL is the inbound path that selects this endpoint: a rooted path such as /api/webhook, never a full URL. URL is the outbound backend address the ESB forwards to, and must be an absolute http:// or https:// URL.
Both are validated when the endpoint is saved, and the error names the other field if they are swapped. A consume URL must also be unique within an application: the ESB selects an endpoint by that path, so two endpoints sharing one would make both unreachable.
2. Create and Configure Machine User
Create a machine user and grant it access to the endpoint through the Machine User Access tab.
3. Call the ESB Endpoint
curl -X POST https://manager.example.com/esb/{appId}/api/webhook \
-H "Authorization: Bearer {machine-user-token}" \
-H "Content-Type: application/json" \
-d '{"event": "user.signup", "userId": 12345}'API Endpoint
Endpoint Pattern: /esb/{applicationId}/{endpointPath}
Any HTTP method is accepted. Authentication is a machine-user Bearer token (Authorization: Bearer <token>); Basic authentication is rejected on this route. applicationId is the application's UUID and endpointPath is the consume URL configured on the endpoint (exact match).
Message Transformation
Endpoints can define Go templates to transform request bodies before forwarding. Responses from the backend are not transformed.
Template Context
Templates receive these variables:
.Data is the parsed request body. With input type JSON it is the decoded JSON value; with XML/SOAP it is the decoded element tree described below. If no input type is set, the body is tried as JSON first; a body that is not JSON is wrapped as {"Body": "<raw text>"}, so reach it as .Data.Body.
.Application describes the application:
.Application.ID- Application UUID.Application.Name- Application name.Application.Slug- Application slug.Application.Description- Application description
.Endpoint describes the endpoint:
.Endpoint.ID- Endpoint UUID.Endpoint.Name- Endpoint name.Endpoint.ConsumeURL- Inbound path that selects this endpoint.Endpoint.URL- Target backend URL this endpoint forwards to.Endpoint.Description- Endpoint description
.User describes the calling machine user:
.User.ID- Machine user UUID.User.Username- Machine user username.User.Name- Machine user display name
.Meta is request metadata:
.Meta.Timestamp- RFC 3339 timestamp string.Meta.RequestTime- Go time.Time object
.Env holds the allowlisted environment variables (PRODUCTIFY_ENV, PRODUCTIFY_REGION, APP_ENV, DEPLOYMENT_ENV)
Template Example
{
"event": "{{ .Data.Event | jsonEscape }}",
"timestamp": "{{ .Meta.Timestamp }}",
"application": {
"name": "{{ .Application.Name }}",
"slug": "{{ .Application.Slug }}"
},
"user": "{{ .User.Username }}",
"data": {{ .Data.Payload | toJson }},
"source": "{{ .Data.Source | toUpper | jsonEscape }}"
}Available Template Functions
toJson- Convert value to JSON stringtoString- Convert value to stringtoUpper- Convert string to uppercasetoLower- Convert string to lowercasenow- Get current timestamp in RFC3339 formatenv- Get environment variable by key (allowlisted variables only)jsonEscape- Escape a value for embedding inside a JSON string literalxmlEscape- Escape a value for embedding inside XML/SOAP element text
Always escape caller data
Anything reached through .Data came from the caller. Interpolating it raw into a JSON string literal or an XML element lets a caller break out of the literal and rewrite the message your backend receives:
// UNSAFE - a caller sending {"name": "\", \"admin\": true, \"x\": \""}
// injects new fields into the forwarded object
{"name": "{{ .Data.name }}"}
// SAFE
{"name": "{{ .Data.name | jsonEscape }}"}Use jsonEscape inside JSON string literals and xmlEscape inside XML or SOAP element text. Numeric and boolean values interpolated outside quotes do not need escaping. Values that do not come from the caller (.Application.Slug, .Meta.Timestamp) are platform-controlled and safe as-is.
Input/Output Message Types
Configure how the ESB processes messages:
- JSON - Parse/output as JSON (default)
- XML - Parse/output as XML
- SOAP - Parse/output as SOAP XML with appropriate content type
XML and SOAP bodies decode into nested values addressable with ordinary template syntax. Elements are keyed by their local name, so a namespace-prefixed <soap:Body> is reached as .Data.Envelope.Body: write the local name, not the wire prefix. Repeated sibling elements become a list you can range over, and attributes appear under an @ prefix:
{{/* <Order currency="EUR"><Items><Item><Name>widget</Name></Item></Items></Order> */}}
{{ index .Data.Order "@currency" }}
{{ range .Data.Order.Items.Item }}{{ .Name | jsonEscape }}{{ end }}Output type sets the forwarded Content-Type
outputMessageType selects the Content-Type the ESB sends to your backend along with the rendered body. It does not transform the backend's response; responses are relayed to the caller unchanged apart from stripping Set-Cookie and hop-by-hop headers.
Testing a Template
Templates are Go templates, and a mistake in one surfaces at runtime as a 500 on a live call. Preview a template against a sample body before saving it, using the previewESBTransform mutation:
mutation {
previewESBTransform(
endpointId: "123e4567-e89b-12d3-a456-426614174000"
input: {
body: "{\"type\": \"user.signup\"}"
# Optional: try a candidate without overwriting the stored template
transformationTemplate: "{\"event\": \"{{ .Data.type | jsonEscape }}\"}"
}
) {
ok
output
contentType
error
}
}A template error comes back in error with ok: false rather than failing the mutation. The preview only renders: it never forwards to your backend, never writes an audit row, and never saves the candidate template.
Backend Authentication
When forwarding to backend services, the ESB can apply authentication:
None
No authentication header added.
Bearer Token
Authorization: Bearer {configured-token}Basic Authentication
Authorization: Basic {base64(username:password)}Audit Logging
The ESB supports three audit logging levels:
NONE
No audit logs are created. Use this for high-volume, low-security endpoints to save database space.
BASIC (Default)
Logs metadata only:
- Timestamp and duration
- Source IP address
- Request method and path
- Response status code
- Error messages (if any)
- Whether transformation was applied
Does NOT log: Request/response headers or bodies
FULL
Logs everything including:
- All BASIC level data
- Request headers and body
- Response headers and body
Database Impact
FULL logging can significantly increase database size. Use only for debugging or compliance requirements.
Reading the Audit Trail
Query an application's ESB trail, newest first, optionally narrowed to a single endpoint:
query {
esbAuditLogs(applicationId: "123e4567-e89b-12d3-a456-426614174000", limit: 50) {
timestamp
sourceIP
requestMethod
requestPath
responseStatus
durationMs
targetURL
transformApplied
error
endpoint { id name }
machineUser { id username }
}
}error is the field to reach for first: a transformation or forwarding failure records its reason there and nowhere else the product surfaces. targetURL is the address that was actually dialed, which differs from the endpoint's current url if the endpoint has been edited since.
Reading the trail requires the project maintainer role, a higher bar than viewing endpoint configuration, because these rows describe the tenant's own traffic.
Request and response bodies and headers are not exposed through this query even at FULL log level. They are redacted on write, but redaction filters known secret shapes rather than guaranteeing anything about arbitrary payloads, so they are stored for forensic access and deliberately kept off the API.
Processing Flow
1. Client sends request to /esb/{appId}/{endpointPath}
└─> Authorization: Bearer {token}
2. Machine user authentication middleware validates token
└─> Checks if token is valid and machine user is enabled
3. ESB handler finds endpoint by application ID and consume URL
└─> Verifies endpoint is enabled
└─> Checks machine user has access to endpoint
4. Transform request (if template configured)
└─> Parse input based on input_message_type
└─> Apply Go template transformation
└─> Set output content type
5. Forward to the endpoint's target URL (the `url` field)
└─> Apply endpoint authentication (Bearer/Basic/None)
└─> Preserve original headers, except Authorization, hop-by-hop
headers and the proxy-managed identity set (X-User-*, X-Tenant-*,
X-Auth-Type, X-Token-*), which a caller must never be able to spoof
└─> Execute HTTP request
6. Return response to client
└─> Copy response headers, stripping Set-Cookie and hop-by-hop headers
└─> Return response body and status code
7. Audit log transaction (based on log_level)
└─> Store in esb_audit_logs table
└─> Include timing, errors, transformation status
└─> Include bodies/headers if log_level is FULLError Responses
The ESB returns appropriate HTTP status codes:
400 Bad Request- Invalid application ID or unreadable request body401 Unauthorized- Invalid or missing machine user credentials, or a non-BearerAuthorizationheader403 Forbidden- Machine user doesn't have access to endpoint404 Not Found- Endpoint not found or disabled409 Conflict- Two endpoints in this application share the consume URL, so the route is ambiguous; remove or repath the duplicate413 Payload Too Large- Request body exceeds the configured limit429 Too Many Requests- Repeated failed authentications from this client tripped the machine-auth throttle; wait for theRetry-Afterheader500 Internal Server Error- Transformation failed502 Bad Gateway- Failed to forward request to backend, or the endpoint has no target URL configured
All errors are logged to the audit database (if logging is enabled).
Security Considerations
- Use strong, unique machine-user tokens and grant each one only the endpoints it needs.
- Use TLS for the Manager and for every backend URL.
- Monitor the audit logs for unexpected callers or targets.
- Prefer BASIC logging where request bodies may contain sensitive data.
- Pipe every caller-supplied value through
jsonEscapeorxmlEscape; see the warning under Available Template Functions.
Configuration Example
GraphQL Mutation
mutation {
createEndpoint(
applicationId: "123e4567-e89b-12d3-a456-426614174000"
input: {
name: "API Gateway"
description: "Forwards API requests to backend"
consumeURL: "/api/data"
url: "https://backend.service.com/api/data"
enabled: true
inputMessageType: JSON
outputMessageType: JSON
authType: Bearer
authToken: "backend-token-xyz"
logLevel: BASIC
transformationTemplate: "{\"event\": \"{{ .Data.type | jsonEscape }}\", \"data\": {{ .Data.payload | toJson }}}"
}
) {
id
name
}
}Best Practices
Performance
- Use BASIC logging for production endpoints
- Use NONE logging for high-volume, non-critical endpoints
- Keep transformation templates simple
Security
- Rotate machine user tokens regularly
- Limit machine user access to required endpoints only
- Use HTTPS for all backend URLs
- Review audit logs regularly
Debugging
- Preview a template with
previewESBTransformbefore saving it, rather than discovering a template error from a caller's500 - Read
esbAuditLogsand check theerrorfield first - Use FULL logging temporarily when debugging issues
- Switch back to BASIC after resolving issues
- Monitor database size when using FULL logging
Related Documentation
- Endpoints - creating and editing endpoints in the UI
- Machine Users - the credential ESB callers use
- REST API - every Manager route and its authentication
- Backend Registration - the other way a backend talks to the Manager