# ADR-003 — Plug-based Pipeline for Request Processing

**Date:** 2026-04-26
**Status:** Accepted
**Deciders:** Architecture Team

---

## Context

Every inbound request — regardless of which gateway received it — must pass through a defined
sequence of processing stages:

1. Authentication (who is this?)
2. Rate limiting (are they allowed to call this much?)
3. Inbound transformation (normalise to canonical format)
4. Route resolution (which adapter handles this?)
5. Adapter dispatch (call the south-side system)
6. Outbound transformation (map response to client format)
7. Audit logging (record what happened)

This sequence must be consistent, individually testable, and extensible without touching all
stages when adding a new one.

---

## Decision

Model the core processing pipeline as a **`Plug.Builder` chain** inside `mw_router`.

Each stage is an independent Plug module. Gateways hand off to the pipeline after initial
protocol parsing:

```elixir
defmodule MwRouter.Pipeline do
  use Plug.Builder

  plug MwAuth.Plug
  plug MwRouter.RateLimiter
  plug MwTransform.InboundPlug
  plug MwRouter.RoutePlug
  plug MwRouter.Dispatcher
  plug MwTransform.OutboundPlug
  plug MwAudit.Plug
end
```

The `conn` is replaced by a `MwKernel.Context` struct that is passed through the chain,
following the same halt/assign pattern as `Plug.Conn`.

---

## Rationale

### Alternatives Considered

| Option | Description | Rejected Because |
|--------|-------------|-----------------|
| GenServer pipeline | Each stage is a GenServer; passes message down chain | Adds process-boundary overhead; halting early requires message passing back up the chain |
| Function composition | `pipeline = fn ctx -> ctx \|> auth() \|> rate_limit() \|> ...` | No standard halt mechanism; error handling requires each function to check previous result |
| Broadway pipeline | Use Broadway for all requests | Broadway is for async batch; adds back-pressure semantics that are wrong for synchronous request/response |
| Event-driven (PubSub) | Each stage subscribes and publishes | No guaranteed ordering; debugging causal chains is difficult; latency is unpredictable |

### Why Plug?

- Plug is the foundational abstraction already used in all Phoenix applications. The team knows it.
- `Plug.Builder` provides the `halt/1` mechanism for short-circuiting (auth failure, rate limit
  exceeded) without exception-based flow control.
- Each plug is independently testable: `MwAuth.Plug.call(context, opts)` in isolation with
  a mock context.
- New stages can be inserted at any position in the pipeline without changing existing stages.
- Plug pipelines are synchronous by design, which matches the synchronous request/response
  contract of the north-side gateways.

---

## Context Struct

The pipeline operates on `MwKernel.Context`, not raw `Plug.Conn`. This is important because
WebSocket channels and async callbacks also go through the pipeline but have no `Conn`.

```elixir
defmodule MwKernel.Context do
  defstruct [
    :trace_id,       # UUID v7, propagated through all logs and spans
    :tenant_id,
    :user,           # %MwAuth.Identity{} after auth plug runs
    :roles,          # list of atoms
    :request,        # %MwKernel.Message{} inbound
    :response,       # %MwKernel.Message{} after adapter returns
    :adapter,        # resolved adapter module
    :halted,         # boolean — mirrors Plug.Conn.halted
    :assigns,        # map — arbitrary per-stage data
    :errors          # list of %MwKernel.Error{}
  ]
end
```

---

## Async Bypass

Stages 5–7 (dispatch, outbound transform, audit) can be bypassed for async requests.
`adapter_file` and `adapter_dw` in batch mode return `{:async, job_id}` from `send/2`.
The pipeline detects this and:
1. Skips outbound transform
2. Returns `202 Accepted` with `job_id` to client
3. Writes audit event with `status: :accepted`
4. Completion event arrives via PubSub → pushed to `gateway_ws`

---

## Consequences

### Positive
- Consistent processing guarantee regardless of gateway entry point
- Each stage is independently unit-testable
- Halt semantics are explicit and traceable
- Adding a new stage (e.g., fraud scoring) = add one Plug, insert into pipeline

### Negative
- Synchronous chain means adapter latency is always on the critical path for sync requests
  — mitigated by circuit breakers and timeouts at the dispatcher stage
- `MwKernel.Context` must be kept lean; stages must not store large payloads in `:assigns`
