---
title: "ADR-012: SecureHttpClient API and transports"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:adr-012-secure-http-transports@1.0"
source: "Developer/Adr/ADR-012-SecureHttpClientTransports.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# ADR-012: SecureHttpClient API and transports {#adr-012-securehttpclient-api-and-transports}

**Table of contents**

-   [Status](https://docs.typo3.org/permalink/netresearch/nr-vault:status@1.0)
-   [Date](https://docs.typo3.org/permalink/netresearch/nr-vault:date@1.0)
-   [Context](https://docs.typo3.org/permalink/netresearch/nr-vault:context@1.0)
-   [Decision](https://docs.typo3.org/permalink/netresearch/nr-vault:decision@1.0)
-   [Consequences](https://docs.typo3.org/permalink/netresearch/nr-vault:consequences@1.0)
-   [Alternatives considered](https://docs.typo3.org/permalink/netresearch/nr-vault:alternatives-considered@1.0)
-   [Related decisions](https://docs.typo3.org/permalink/netresearch/nr-vault:related-decisions@1.0)

## Status {#status}

Accepted

## Date {#date}

2026-01-12

## Context {#context}

Multiple extensions need to call external HTTP services with centralized credentials,
policies, and audit. We need:

-   a stable, minimal public PHP API,
-   a clean boundary between "product logic" (registry/policy/audit) and "transport engine".

We also want optional Rust (FFI or later sidecar) without forcing it on everyone.

## Decision {#decision}

We define a public `SecureHttpClientInterface` and a transport abstraction:

### SecureHttpClient (product logic) {#securehttpclient-product-logic}

-   Resolves service by `serviceId`
-   Loads credential set
-   Enforces policy (deny-by-default)
-   Executes request via a configured transport backend
-   Records audit metadata
-   Returns a response wrapper

### TransportInterface (engine) {#transportinterface-engine}

**TransportInterface**

```php
interface TransportInterface
{
    public function send(RequestSpec $request): ResponseSpec;
}
```

Backends:

-   `PhpTransport` (default; PSR-18 or Symfony HttpClient)
-   `RustFfiTransport` (optional)
-   (future) `SidecarTransport`

Consumers never talk to transports directly; they only use `SecureHttpClientInterface`.

## Consequences {#consequences}

### Positive {#positive}

-   Decouples governance logic from transport implementation
-   Allows fallback and progressive rollout of Rust
-   Keeps API surface small and stable for consumers
-   Avoids "typed DTO in Rust" trap: response is raw/json in PHP

### Negative {#negative}

-   Slight abstraction overhead
-   Requires careful policy enforcement placement (must not be bypassable)

## Alternatives considered {#alternatives-considered}

### Let consumers pick HTTP client directly {#let-consumers-pick-http-client-directly}

Allow consumers to use PSR-18 clients directly.

**Rejected**: loses central policy enforcement and audit guarantees.

### Make Rust mandatory {#make-rust-mandatory}

Require Rust transport for all installations.

**Rejected**: adoption killer; too many environments can't/won't run native code.

## Related decisions {#related-decisions}

-   [ADR-008: HTTP client](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-008-http-client@1.0) \- Current HTTP client (extended by this decision)
-   [ADR-010: Secure Outbound inside nr-vault](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-010-secure-outbound@1.0) \- Parent decision for Secure Outbound feature
-   [ADR-013: Rust FFI preload-only mode](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-013-rust-ffi-preload@1.0) \- Rust FFI production configuration
