Skip to content

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 ​

bash
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 ​

go
{
  "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 string
  • toString - Convert value to string
  • toUpper - Convert string to uppercase
  • toLower - Convert string to lowercase
  • now - Get current timestamp in RFC3339 format
  • env - Get environment variable by key (allowlisted variables only)
  • jsonEscape - Escape a value for embedding inside a JSON string literal
  • xmlEscape - 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:

go
// 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:

go
{{/* <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:

graphql
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:

graphql
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 FULL

Error Responses ​

The ESB returns appropriate HTTP status codes:

  • 400 Bad Request - Invalid application ID or unreadable request body
  • 401 Unauthorized - Invalid or missing machine user credentials, or a non-Bearer Authorization header
  • 403 Forbidden - Machine user doesn't have access to endpoint
  • 404 Not Found - Endpoint not found or disabled
  • 409 Conflict - Two endpoints in this application share the consume URL, so the route is ambiguous; remove or repath the duplicate
  • 413 Payload Too Large - Request body exceeds the configured limit
  • 429 Too Many Requests - Repeated failed authentications from this client tripped the machine-auth throttle; wait for the Retry-After header
  • 500 Internal Server Error - Transformation failed
  • 502 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 jsonEscape or xmlEscape; see the warning under Available Template Functions.

Configuration Example ​

GraphQL Mutation ​

graphql
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 previewESBTransform before saving it, rather than discovering a template error from a caller's 500
  • Read esbAuditLogs and check the error field first
  • Use FULL logging temporarily when debugging issues
  • Switch back to BASIC after resolving issues
  • Monitor database size when using FULL logging