Skip to content

Ports and Adapters

What are Ports?

Ports are interfaces that define how the application layer communicates with the outside world. They live in the domain/core layer and have zero knowledge of concrete implementations.

NEKTE defines 5 core ports:

PortDirectionPackagePurpose
TransportOutbound@nekte/clientSend requests, receive responses
DelegateHandlerInbound@nekte/serverHandle delegated tasks with streaming
CacheStoreOutbound@nekte/clientStore/retrieve cached capabilities
AuthHandlerInbound@nekte/serverAuthenticate incoming requests
StreamWriterOutbound@nekte/serverWrite SSE/gRPC streaming events

Port Contracts

Transport (Client-side)

The primary outbound port for the client. Every transport must implement request-response semantics.

// @nekte/client
export interface Transport {
request(req: NekteRequest): Promise<NekteResponse>;
close(): Promise<void>;
}

Adapters:

  • HttpTransport — HTTP POST with JSON or MessagePack
  • GrpcClientTransport — gRPC unary calls + server-streaming for delegate

DelegateHandler (Server-side)

The inbound port for task delegation with streaming. Handlers receive a stream writer, context, and abort signal.

// @nekte/server
export type DelegateHandler = (
task: DelegateTask,
stream: StreamWriter,
context: HandlerContext,
signal: AbortSignal,
) => Promise<void>;

CacheStore

Outbound port for capability caching. The application layer calls this port; the adapter decides storage (memory, Redis, etc.).

// @nekte/client
export interface CacheStore {
get(key: string): CacheGetResult | undefined;
set(key: string, value: CachedCapability, ttl: number): void;
delete(key: string): void;
clear(): void;
}

Adapters:

  • InMemoryCacheStore — SIEVE eviction + GDSF token-cost weighting

AuthHandler

Inbound port for request authentication. Returns the validated identity or throws.

// @nekte/server
export interface AuthHandler {
authenticate(req: IncomingMessage): Promise<AuthResult>;
}

Adapters:

  • bearerAuth(validate) — Bearer token validation
  • apiKeyAuth(validate) — API key in header
  • noAuth() — Pass-through (development)

StreamWriter (SSE/gRPC)

Outbound port for streaming delegate events. The server writes events; the transport decides encoding.

// @nekte/server
export interface StreamWriter {
progress(data: ProgressEvent): void;
partial(data: PartialEvent): void;
complete(data: CompleteEvent): void;
cancelled(data: CancelledEvent): void;
suspended(data: SuspendedEvent): void;
resumed(data: ResumedEvent): void;
error(data: ErrorEvent): void;
}

Adapters:

  • SseStream — Server-Sent Events over HTTP
  • GrpcDelegateStream — gRPC server-streaming

Adapter Implementations

HttpTransport

The default client transport. Sends JSON-RPC requests over HTTP POST.

import { NekteClient } from '@nekte/client';
// Default: uses HttpTransport automatically
const client = new NekteClient('http://localhost:4001');

GrpcTransport

Native gRPC with protobuf encoding. Uses server-streaming for delegate operations.

import { NekteClient, createGrpcClientTransport } from '@nekte/client';
const transport = createGrpcClientTransport('localhost:50051');
const client = new NekteClient('grpc://localhost:50051', { transport });

InMemoryCacheStore

Combines SIEVE eviction with GDSF token-cost weighting. L2 schemas (120 tokens) survive eviction over L0 refs (8 tokens).

import { NekteClient } from '@nekte/client';
const client = new NekteClient('http://localhost:4001', {
cache: {
maxEntries: 1000,
defaultTtlMs: 300_000, // 5 min
swr: true, // stale-while-revalidate
},
});

The Anti-Corruption Layer

Each adapter includes an anti-corruption layer that translates between external representations and domain types:

  • gRPC: fromProtoDiscoverRequest() / toProtoDiscoverResponse() convert between protobuf and domain types
  • HTTP: JSON-RPC envelope wrapping/unwrapping
  • SSE: Event type discrimination and serialization
  • MCP Bridge: Full MCP-to-NEKTE translation with schema compression

This ensures that domain logic never depends on wire format details.