Files
tech-log-backend/docs/superpowers/specs/2026-08-02-conditional-inbound-transport-boundary-design.md

6.5 KiB

Conditional Inbound Transport Boundary Design

Date: 2026-08-02
Status: approved by the user's instruction to apply the detailed P1/P2 review sequentially
Scope: the opt-in GraphQL, gRPC, and WebSocket leaf modules and their release evidence

Context

The three leaves are registered and tested independently, but neither app-bootstrap nor sample-portfolio has a production dependency on them. That omission is intentional: adding a classpath edge today would activate GraphQL, start a plaintext/reflection-enabled gRPC server by default, and unconditionally expose a wildcard-origin STOMP broker that serializes arbitrary domain events. The leaf documentation nevertheless describes sample contributions that do not exist, and the ordinary root check can become NO-SOURCE without a transport-specific positive-count and zero-skip qualification gate.

P1 therefore makes opt-in status executable and makes accidental activation fail closed. It does not add these leaves to the default runtime or claim the P2 production baselines.

Runtime Membership SSOT

Every entry in config/architecture/modules.json gains an exact runtime_memberships array whose values are limited to the two composition roots: app-bootstrap and sample-portfolio.

  • A composition root includes itself in its membership.
  • Direct production api/implementation/compileOnly/runtimeOnly project dependencies must equal the registry members for that root, excluding the root itself.
  • An empty array means the leaf is built and architecture-checked but absent from both shipped runtime graphs. GraphQL, gRPC, WebSocket, and Mongo remain in this state.
  • Test fixtures and custom qualification configurations do not change production membership.

Settings validation is fail-closed for missing, duplicate, or unknown membership names. A Gradle verification task compares the registry to both composition roots and is part of check.

Explicit Qualification Composition

app-bootstrap owns a conditionalTransportTest source set whose classpath explicitly includes the three opt-in leaves. It proves that the opt-in artifacts resolve together while the registry still declares them absent from both default runtime graphs. It is evidence composition, not a new production dependency edge.

The root registers exact qualification Test tasks for GraphQL, gRPC, and WebSocket. Each task:

  • names required test classes rather than broad discovery;
  • fails on no match or no discovery;
  • always reruns in UTC;
  • fails if the root suite reports any skipped test.

An aggregate conditionalTransportQualification task depends on the composition contract and all three exact lanes. CI invokes it explicitly from the existing release-blocking quality job, and the gate matrix records the task.

gRPC P1 Boundary

gRPC activation becomes explicit and local-only until a later TLS/mTLS design exists:

  • enabled=false and reflectionEnabled=false are defaults; missing properties create no runner, health manager, reflection service, or listener.
  • The current insecure credential mode requires an explicit local-development override and a loopback bind address. Non-loopback insecure bind fails startup.
  • Feature services require a caller-supplied authentication policy/interceptor. Missing or invalid metadata returns stable UNAUTHENTICATED; valid metadata reaches the service.
  • Health remains a local lifecycle probe; reflection is a separate explicit flag.
  • The error interceptor wraps ServerCall.close, so handler throws, listener throws, ordinary responseObserver.onError, and raw StatusRuntimeException all pass the same sanitizer. Recognized ApiErrorCarrier causes produce stable code/category trailers; unrecognized status descriptions become fixed INTERNAL_ERROR with no raw diagnostic.

A real ephemeral Netty unary service verifies authentication, reflection-off, all error paths, and sentinel redaction. TLS/mTLS, external bind, deadlines, streaming, and protobuf compatibility are P2 and remain unclaimed.

GraphQL P1 Boundary

GraphQL remains classpath-selected: its absence from the default runtime is the disable mechanism, and the qualification classpath is the explicit opt-in mechanism. The wire lane starts a real random-port MVC server and crosses HTTP JSON, Spring Security, and CORS.

It verifies unauthenticated rejection, authenticated health success, allowed/disallowed origins, GraphiQL disabled, production-style introspection disabled, stable carrier errors, unknown errors, and absence of distinct secret sentinels from the complete response body. The existing resolver is changed only if a failing wire contract proves unsafe behavior.

Feature schema/resolvers, field authorization, depth/cost, persisted queries, DataLoader, schema compatibility, and subscriptions remain P2.

WebSocket P1 Boundary

WebSocket gains ca-skeleton.websocket.enabled=false; both configuration and broadcaster are conditional. Enabled settings reject wildcard/blank origins and invalid endpoint/destination shapes.

The inbound channel requires an authenticated handshake principal, permits subscription only to the configured server topic, permits authenticated application sends under /app/**, and rejects client sends to /topic/**. A custom STOMP error handler emits only a fixed client-safe code.

The broadcaster no longer serializes arbitrary @DomainEvent objects. It consults an explicit projection allowlist; an event without exactly one projection is not sent. Projection output is a bounded primitive map, not the domain object graph.

A real random-port WebSocket/STOMP lane verifies disabled absence, origin/auth/connect/subscribe, server push, broker-send rejection, error redaction, and no projection/no broadcast. The simple broker remains local/R1 only; broker relay, cross-node durability, replay, backpressure, and a domain-specific versioned projection catalog remain P2.

Documentation Truthfulness

Leaf READMEs and CLAUDE files describe only code that exists. Sample GraphQL schemas, gRPC services, and WebSocket publishers are future adoption examples, not current runtime features. Each document states the activation switch, exact P1 evidence, and unimplemented P2 limits.

Non-Goals

  • adding any of the three leaves to a shipped default runtime;
  • adding a production project dependency edge outside the registry;
  • claiming production readiness from local loopback/simple-broker tests;
  • implementing sample feature APIs or domain payloads;
  • staging, committing, amending, or pushing changes.