Unified Microservice Channel
1. Overview
This document defines the target contract between light-4j/portal-registry and the controller backend.
The main goals are:
- use a single registration payload for both
light-controllerandcontroller-rs - allow a service to use one WebSocket channel for both registration and discovery
- use the client-supplied service address instead of deriving the service address from the remote socket IP
- enforce tenant isolation with a controller-configured
hostIdand the JWThostclaim - align runtime instance persistence with existing
RuntimeInstanceCreatedEvent,RuntimeInstanceUpdatedEvent, andRuntimeInstanceDeletedEvent
2. Core Decisions
2.1 Single Service Channel
For microservices, the controller should expose one WebSocket channel:
/ws/microservice
This channel is used for:
service/registerservice/update_metadatadiscovery/lookupdiscovery/subscribediscovery/unsubscribe- controller-to-service commands and service responses
For client applications that do not register as services, the controller should expose:
/ws/discovery
This channel is discovery-only.
2.2 Client-Supplied Address
The service address advertised to discovery must come from the registration payload.
The controller must not use the remote socket IP as the canonical service address because real deployments can involve:
- NAT
- reverse proxies
- ingress controllers
- sidecars
- container overlays
The remote socket IP can still be recorded for logging and audit, but it must not be used for routing or discovery.
2.3 Unified Control Plane Channel
For administrative operations, AI agents, and the Portal UI, the controller should expose a dedicated MCP-compliant channel:
/ctrl/mcp
This channel is used for:
- Administrative Tools: MCP
tools/callfor server info, log level updates, configuration reloads, etc. - Real-time Notifications: Controller-to-client notifications for runtime instance lifecycle changes (
instance_connected,instance_disconnected). - AI-Native Operations: Discovery and execution of controller capabilities via the Model Context Protocol.
2.4 One Controller per Tenant
Each controller instance serves exactly one tenant.
The controller configuration contains the authoritative internal tenant identifier, hostId. Services do not send hostId in the registration payload.
The service JWT carries the tenant in the host claim. The controller validates jwt.host == configured hostId.
This avoids cross-tenant routing mistakes and keeps the controller state, discovery subscriptions, and event streams tenant-scoped by construction.
3. Unified Registration Contract
The service/register request should have the same payload for both controller implementations.
Example:
{
"jsonrpc": "2.0",
"id": "register-1",
"method": "service/register",
"params": {
"jwt": "<service-jwt>",
"serviceId": "com.networknt.user-1.0.0",
"envTag": "prod",
"version": "1.0.0",
"protocol": "https",
"address": "10.0.5.12",
"port": 8443,
"tags": {}
}
}
Required behavior:
addressis the canonical advertised service addressportis the advertised service portjwtauthenticates the service sessionserviceIdmust match the service identity in the JWTenvTagis the only environment field in the unified registration contractenvTagshould match the JWTenvclaim when that claim is present
portal-registry should build one registration payload for all controller backends. There should be no separate toControllerRsRegisterParams() path.
4. Authentication and Tenant Isolation
4.1 /ws/microservice
Authentication for /ws/microservice is performed with the JWT inside service/register.
The service JWT must be validated for:
- signature
- issuer and audience when configured
- service identity
- tenant identity
The controller configuration contains a single internal tenant identifier, hostId. The service JWT must contain the same tenant identifier in the host claim.
Recommended claim handling:
- read
host
If the JWT host claim does not match the controller-configured hostId, the controller must reject the registration. In other words, the JWT tenant claim host is validated against the controller’s internal tenant key hostId. This prevents a service from connecting to the wrong tenant controller instance.
4.2 /ws/discovery
Client applications that use /ws/discovery must authenticate on the WebSocket upgrade request with:
Authorization: Bearer <jwt>
The discovery JWT should also be checked using the host claim against the controller-configured hostId so discovery clients cannot attach to the wrong tenant controller.
4.3 No Extra Discovery Token for Services
Once a service is connected on /ws/microservice, no separate discovery token is needed for service-owned discovery requests on that same socket.
The separate discovery bearer token remains relevant only for discovery-only clients using /ws/discovery.
4.4 /ctrl/mcp
Authentication and authorization for the control plane channel are performed at the gateway (BFF) level or directly on the controller via the WebSocket upgrade request:
Authorization: Bearer <user-jwt>
Requirements:
- User must have administrative or operator roles.
- The JWT
hostclaim must match the controller-configuredhostIdto ensure tenant isolation. - Connections should be restricted to known administrative origins (e.g., the Portal UI).
5. Runtime Instance Identity
5.1 Business Key
Before emitting RuntimeInstanceCreatedEvent, the controller must query the database using this business key:
hostIdfrom controller configuration, validated from the JWThostclaimserviceIdenvTagaddressport
If a matching runtime instance already exists, the controller must reuse its runtimeInstanceId.
If no matching runtime instance exists, the controller must create a new UUID for runtimeInstanceId.
5.2 Aggregate Identity
The event aggregate identity is:
hostId|runtimeInstanceId
This matches the existing Light Portal aggregate-id derivation for runtime instance events. Event payloads and persistence continue to use hostId, even though JWT validation reads the tenant from the host claim.
6. Event Persistence
6.1 Event Types
The controller should align to the existing event types:
RuntimeInstanceCreatedEventRuntimeInstanceUpdatedEventRuntimeInstanceDeletedEvent
At present, the primary lifecycle use cases are:
- create on successful registration
- delete on disconnect or explicit removal
RuntimeInstanceUpdatedEvent should remain supported by the contract, but it does not need to be emitted until there is a concrete mutable metadata use case.
6.2 Create Flow
After a successful service/register:
- resolve
hostIdfrom controller configuration after validating the JWThostclaim - query by
hostId + serviceId + envTag + address + port - reuse existing
runtimeInstanceIdif found, otherwise create a new UUID - determine the next aggregate version for
hostId|runtimeInstanceId - persist
RuntimeInstanceCreatedEventtoevent_store_t - persist the matching integration message to
outbox_message_t
The downstream processing of RuntimeInstanceCreatedEvent should perform an upsert into runtime_instance_t.
6.3 Delete Flow
When the /ws/microservice socket closes:
- locate the current runtime instance within the controller’s configured
hostId - determine the next aggregate version for
hostId|runtimeInstanceId - persist
RuntimeInstanceDeletedEventtoevent_store_t - persist the matching integration message to
outbox_message_t
The downstream projection should mark the runtime instance as disconnected or inactive in runtime_instance_t.
6.4 Update Flow
RuntimeInstanceUpdatedEvent is reserved for future use.
It should only be emitted if there is a real business need to update mutable runtime metadata after registration.
If the service address or port changes, that should normally be treated as a different runtime identity rather than an in-place update.
7. Discovery Semantics
Discovery requests on /ws/microservice and /ws/discovery use the same runtime registry state.
Discovery operations:
- do not create runtime instance lifecycle events
- do not affect runtime identity
- are session-level behavior, not aggregate lifecycle changes
On disconnect:
- a
/ws/microservicedisconnect removes the service session and emitsRuntimeInstanceDeletedEvent - a
/ws/discoverydisconnect only clears that discovery client’s subscriptions
8. Migration Impact
8.1 controller-rs
controller-rs should be updated to:
- accept
addressinservice/register - use request
addressas the canonical advertised address - keep remote socket IP for logging and audit only
- validate JWT
hostclaim against controller configuration - allow discovery requests on
/ws/microservice - keep
/ws/discoveryfor discovery-only clients - move runtime instance persistence toward the Light Portal aggregate and tenant model
8.2 light-controller
light-controller already accepts request address with remote-address fallback. It should move to the same stricter tenant model:
- controller-configured internal tenant key
hostId - JWT
hostclaim match required - one tenant per controller instance
8.3 portal-registry
portal-registry should be simplified to:
- send one
service/registerpayload shape to both backends - use
/ws/microserviceas the only channel for service registration and service-owned discovery - stop relying on a separate service-side discovery token for normal service use
8.4 Control Plane Consolidation
Transition the Portal UI from separate REST polling and event sockets to the unified /ctrl/mcp channel. Real-time hydration and updates should be delivered via MCP notifications.
9. Summary
The target model is:
- one controller instance per tenant
- one service socket on
/ws/microservice - one discovery-only socket on
/ws/discoveryfor non-service clients - one unified control plane socket on
/ctrl/mcpfor the Portal UI and AI agents - one shared registration payload for all controller backends
- client-supplied
addressas the canonical service address - controller-configured
hostIdenforced by validating it against the JWThostclaim - runtime instance lifecycle persisted with existing runtime instance events and aggregate versioning
- administrative actions and real-time dashboard updates consolidated into MCP tools and notifications