Skip to main content
A backend binds a protocol to a real external provider. It supplies the provider’s host and credentials, an authentication strategy, and the mapping rules that translate each protocol action into the provider’s wire format — and the provider’s response back into the protocol’s shape. One protocol can have many backends. That is the core of Link: define the contract once, then point it at as many providers as you need, and switch or combine them without changing the caller.
In a Specter integration, a Link backend is the external risk engine Specter calls. Specter reaches it over the specter-v1 protocol — see Specter backends.

Anatomy

  • id — a short identifier you choose, used to address the backend (for example when disambiguating an invocation).
  • protocol — the protocol’s $id URL; must match an imported protocol.
  • host — the provider’s hostname (no scheme, no path).
  • credentials — consumed by the authentication pipeline; encrypted at rest.
  • auth_pipeline — how outbound requests are authenticated (see below).
  • connections — a map from action (or action.variant) to its mapping configuration.
  • tls — optional: the client certificate the backend presents (see Client certificates).
Backend hosts are checked by an SSRF guard when a backend is created or updated (imported protocol URLs are checked the same way). The URL must use https, the host must resolve, and every resolved IPv4 and IPv6 address must fall outside the reserved ranges — private, loopback, link-local, multicast, carrier-grade NAT, and IPv6 tunneling prefixes (Teredo, 6to4, NAT64). The check runs at configuration time, not on each invocation.

Authentication strategies

The auth_pipeline determines how Link authenticates outbound calls to the provider. A non-empty pipeline requires a source_type (inline or vault — where the credentials come from) and a credential_type:
Auth-pipeline headers (HMAC Digest, etc.) take precedence over mapped headers on conflict, so signing guarantees are never overwritten.

Credentials and secrets

Credentials are held either inline (encrypted at rest in the backend record) or pulled from a secrets store through a pointer, so the secret value itself never lives in the backend document — Link fetches it at request time. Inline credentials can be rotated in place with the rotate-credentials endpoint, without recreating the backend.

Provisioning: self vs. managed

A backend declares a provisioning mode:
  • self (default) — you own and manage the backend and its credentials.
  • managed — the backend is provisioned and operated by Hellgate on your behalf (an optional service). Its create, update, and delete operations require the additional admin:managed-backends:write scope on top of admin:backends:write.
Credentials are never returned by the admin read endpoints, regardless of provisioning mode.

Mapping

Mapping is declarative: adapter logic lives in versioned templates, not in code. Each connection has a request_mapping and a response_mapping.

Template grammar

Mapping leaves are plain JSON values. Strings carry a mustache-style grammar — zero or more {{ expr }} holes inside a literal string — where expr is a scoped path followed by an optional chain of filters:
Paths are scoped to a root: Filters chain with |. Common filters include required (abort the call if the value is missing), default(<path or literal>) (fall back to another value), omit_if_null (drop the key when null), map({ FROM: TO }) (translate enum values), to_int, and first / last / prefix for string slicing. A map value can also be a path, which Link resolves only when that entry matches. An optional default: argument, a literal or a path, applies when the input is not a key of the dict or is null. Without default:, an input that matches no key passes through unchanged. Chained, two map filters combine two provider fields into one protocol value:
Here an actionCode of 00 passes the provider’s nameMatch on to the second map, and any other code, a missing value, or an unknown match result ends in BLOCK. Every path in a template is checked against its scope, including paths inside filter arguments: a request mapping that uses $.response.* anywhere, for example in default(...), is rejected when you save the backend.

Request mapping

request_mapping mirrors the provider’s expected request. It has an optional body (a nested object matching the provider’s shape) and headers (a flat map of header name to template):

Response mapping

response_mapping is keyed by the provider’s HTTP status (200, 4xx, default, …). Each entry has a return (the HTTP status Link returns to the caller) and a body template that produces the protocol-shaped result:
Response body templates are validated at backend-write time against the protocol’s response schema for the given return code — missing required keys, unknown keys, and unmatched union branches are rejected before the backend goes live.

Connection keys

A connection key is either the bare action name (assess) or action.variant (assess.pan) when the action has a discriminator. Link validates connections against the imported protocol and rejects a backend that misses a required action or variant.

Mocks

Instead of live mapping, a connection can declare mock responses — match conditions plus canned responses. When present, invocations are matched against the mocks and served without any external call. Mocks are ideal for local development, tests, and protocol fixtures.

Message-level encryption

Some providers require every request and response body to be encrypted on top of TLS (JWE). A backend can carry an encryption block that makes Link encrypt outbound bodies and decrypt inbound ones transparently, so callers keep sending and receiving plain JSON. Encryption keys are managed separately and support rotation. Link decrypts every response that carries the encryption envelope, whatever its status. Some providers answer errors unencrypted: a non-2xx response without the envelope is mapped as received, through the connection’s response_mapping, as for a backend without encryption. A 2xx response without the envelope fails with 502 and the code DECRYPT_FAILED, because a successful answer must be encrypted.

Client certificates (mutual TLS)

Some providers accept a call only when the client presents a certificate they issued. Link stores that certificate and its private key as a key with purpose client_tls, and a backend names the key in its tls field. Every call to that backend then presents the certificate in the TLS handshake. Backends without tls present no client certificate.

Upload the certificate

Managing keys requires the admin:keys:write scope. The key_material of a client_tls key is one PEM bundle, in this order:
  1. The certificate issued to you (the leaf), first.
  2. Its intermediate certificates, in chain order, if the provider issues any.
  3. Exactly one unencrypted RSA or EC private key that belongs to the leaf. PKCS#1 (RSA PRIVATE KEY), SEC 1 (EC PRIVATE KEY), and PKCS#8 (PRIVATE KEY) forms are accepted. An EC PARAMETERS block is allowed and ignored.
A client_tls key has no algorithm and no encryption_method: leave both out, because setting either one is rejected. Leave out not_before and not_after too: Link takes them from the leaf certificate, and setting either one is rejected (null is accepted).
Link answers 201 with the key’s metadata. algorithm and encryption_method are null, not_before and not_after are the leaf certificate’s validity, and the key material is never returned:
Link checks the bundle on upload, and answers 422 VALIDATION_ERROR on key_material when it is not PEM, holds no certificate, holds no private key or more than one, holds an encrypted key or a key that is neither RSA nor EC (for example Ed25519), holds any other kind of entry, or when the private key does not match the first certificate. A bundle that lists an intermediate before the leaf fails for that reason. It also fails when any certificate in the bundle, the leaf, an intermediate or a root, has expired or is not yet valid. The messages never repeat the material or a date from it. Like every key, the material is encrypted at rest and never appears in responses, logs, or error results.
algorithm and encryption_method in key responses can now be null: they are null for every client_tls key, in the REST key responses and in the GraphQL algorithm and encryptionMethod fields. If your integration reads these fields from every key, accept null, or skip keys whose purpose is client_tls.

Reference the key from a backend

Set tls.client_key_kid when you create the backend, or when you update a disabled backend:
  • The key must exist and have purpose client_tls. Otherwise the save fails with 422 VALIDATION_ERROR.
  • tls is a closed object: client_key_kid is required and must be a non-empty string, and any other field is rejected.
  • "tls": null removes the reference.
  • A client_tls key cannot be used in a backend’s encryption block.
The backend detail response returns tls, with null for a backend without one. A key that any backend’s tls.client_key_kid names, enabled or not, cannot be deleted: DELETE /api/admin/keys/{id} answers 409 CONFLICT and the key stays. Point the disabled backend’s tls at another key, or set it to null, first.

How calls change

  • Every call presents the leaf certificate and its chain, and proves possession of the private key in the handshake.
  • Calls with a client certificate use HTTP/1.1. Their idle connections close after 5 minutes without a request, and the next call opens a new connection.
  • If the key cannot be used when a call is made, Link sends no request. The result is an error with "source": "transport", status 502, and the code :client_tls_key_not_found (the key no longer exists or no longer has purpose client_tls) or :client_tls_key_invalid (the stored material holds no usable certificate and key).
  • If the provider rejects the certificate, for example because it does not trust its issuer, the call also fails as a transport error with status 502.
For every outbound HTTPS call, backend calls and protocol imports alike, Link verifies the server certificate against the operating system’s trust store and checks it against the hostname. Configuration cannot turn this verification off or replace the trusted certificates, and only a backend’s tls can add a client certificate. A custom server CA per backend is not supported.

Rotate the certificate

An enabled backend accepts no change other than being disabled, so a rotation takes four steps, and the backend takes no traffic between steps 2 and 4:
1

Rotate the key

POST /api/admin/keys/{id}/rotate with a new kid and the new bundle. The new key is validated like a new upload, including the validity of every certificate, and takes the new leaf’s not_before and not_after. The old key stays.
2

Disable the backend

PUT /api/admin/backends/{id} with {"enabled": false}.
3

Point the backend at the new key

PUT /api/admin/backends/{id} with {"tls": {"client_key_kid": "<new kid>"}}.
4

Enable the backend

PUT /api/admin/backends/{id} with {"enabled": true}. You can then delete the old key.

Selecting a backend

When several enabled backends implement the same action and variant, the caller disambiguates with a backend query parameter on invocation. See Invocation.

Next steps

Invocation

Call an action, match the method, select a backend, and handle errors.

Protocols

The contract a backend implements.