Skip to main content
Version: v0.25.0 (Latest)

HTTP & Transport

The EDK has two communication systems that serve different roles but share the same command-oriented architecture:

SystemPurposeProtocolAudience
Universal HTTP AdapterExpose public REST APIs to external clientsHTTP RESTBrowsers, mobile apps, third-party integrations
Command TransportExecute commands locally or via remote microservicesHTTP RPC, gRPCInternal service-to-service communication

Both systems are built on the EDK's command pattern. Every operation, generating a key, resolving a DID, validating a credential, is a ServiceCommand with a typed input and output. The difference is how that command reaches the implementation that executes it.

Monolith vs Microservices

The EDK is designed so that the same application code can be deployed as a monolith or as distributed microservices without code changes. The decision is purely a matter of configuration and which modules are on the classpath.

Monolith Deployment

All command implementations run in the same process. Every command resolves to LOCAL execution:

Monolith Deployment

Include all -impl modules in your classpath and every command executes locally. No network overhead, no serialization cost, no separate services to manage.

Microservice Deployment

Individual domains run as separate services. Commands that target a remote domain are transparently forwarded over HTTP RPC or gRPC:

Microservice Deployment

The calling code is identical in both cases. A developer invoking kms.keys.generate writes the same code regardless of whether KMS runs in-process or on a remote node. The transport layer handles serialization, routing, and error mapping transparently.

How It Works

1. Command Invocation

You can invoke commands directly, but you don't have to. Service-layer classes like KmsService or DidService use commands internally, so whether you call a service method or a command, the transport routing applies equally:

// Option 1: Invoke the command directly
val result = generateKeyCommand.execute(
GenerateKeyInput(algorithm = "EC", curve = "P-256")
)

// Option 2: Use the service layer — internally delegates to the same command
val key = kmsService.generateKey(algorithm = "EC", curve = "P-256")

Both paths end up at the same ServiceCommand. If KMS is configured to run on a remote node, the command is forwarded transparently, regardless of whether you called the command or the service.

This means local vs remote execution is entirely invisible to the caller. You never need to know or care whether a particular domain is in-process or on another machine.

2. Routing Decision

The ExecutionTargetResolver determines where the command runs, based on hierarchical configuration:

TargetBehavior
LOCALExecute in-process via the session-scoped command registry
SERVERForward to a remote service over HTTP RPC or gRPC
AUTOTry the configured remote transport; fall back to LOCAL if unavailable

3. Transport Selection

When a command is routed to SERVER, the configured transport type determines the protocol:

TransportPath ConventionUse Case
HTTPPOST /rpc/{module}/{service}/{action}Cross-platform, firewall-friendly
GRPC{module}.{Service}Service/{Action}High-throughput, streaming support

Both transports carry the same payload, the command's serialized input, and return the same typed result. Switching between them is a one-line config change.

4. Transparent Execution

From the developer's perspective, there is no difference:

// This code works identically whether KMS is local or remote
val key = generateKeyCommand.execute(input)

If KMS is local, the command executes directly. If KMS is remote, the transport layer:

  1. Serializes the input (JSON, CBOR, or Protobuf)
  2. Sends it to the configured endpoint
  3. Deserializes the response
  4. Returns the same IdkResult<T, IdkError> type

Errors from remote services are mapped to the same IdkError types used locally (e.g., NOT_FOUND_ERROR, UNAUTHORIZED_ERROR), so error handling code doesn't need to distinguish between local and remote failures.

Configuration

A single configuration block controls all routing decisions:

sphereon:
transport:
routing:
defaults:
target: LOCAL # Default: everything runs locally
transport: HTTP # Protocol when routing to SERVER
timeoutMs: 30000

modules:
kms: # Route all KMS commands to a remote service
target: SERVER
endpoint: "kms.internal:9090"
transport: GRPC
did:
target: SERVER
endpoint: "https://did-service.internal"
transport: HTTP

This configuration means:

  • All commands default to LOCAL execution
  • kms.* commands are forwarded to kms.internal:9090 via gRPC
  • did.* commands are forwarded to https://did-service.internal via HTTP RPC
  • Everything else (party, credentials, etc.) runs in-process

See Command Transport for the full configuration reference and transport details.

Classpath-Driven Assembly

The transport behavior depends on which modules are on your classpath:

ModuleProvides
lib-transport-routing-apiCore routing logic, local transport, configuration
lib-transport-client-apiHTTP RPC client (Ktor-based)
lib-transport-client-grpcgRPC client (Netty-based, JVM only)
lib-transport-codec-binaryCBOR and Protobuf serialization codecs
Domain -impl modulesLocal command implementations

If a domain's -impl module is on the classpath, its commands can run locally. If the transport client modules are on the classpath and a domain is configured with target: SERVER, those commands are forwarded remotely.

A typical monolith includes all -impl modules. A typical gateway service includes transport clients and only the -impl modules for domains it handles locally.