Skip to content

Cross-Service Integration Guide

This guide walks a developer from a clean machine to a working three-service Virtufin deployment: API Gateway, WebSocketManager, and WorkManager. It covers deploy order, the Dapr components they share, topic ownership, and a minimal single-node deployment.

Scope: The three services covered here are the same trio exposed at api.docs.virtufin.com, websocketmanager.docs.virtufin.com, and workmanager.docs.virtufin.com. Each service has its own detailed documentation; this guide focuses on the wiring between them.

Deploy order

The three services have a strict startup dependency on each other:

1. virtufin-api          (must be first — every other service talks to it)
        |
        v
2. virtufin-websocketmanager  (subscribes to api state + pubsub via api gRPC)
        |
        v
3. virtufin-workmanager       (subscribes to api state + pubsub, plus deploys workers)

Why this order?

  • virtufin-api is the only service that talks to Dapr pubsub and the state store directly. Per the cross-cutting spec §Per-service Pubsub Topics, all state and pubsub operations go through the API's State.* and Pubsub.* gRPC RPCs. The other two services use a Virtufin.Api.Client NuGet wrapper that calls those RPCs.
  • virtufin-websocketmanager is independent of the workmanager — it only needs the API to be up so it can publish connection lifecycle events and read/write its per-service state entry.
  • virtufin-workmanager is independent of the websocketmanager too — it talks only to the API for state, pubsub, and gateway calls (workers route their backend service calls through the API Gateway).

Topic ownership

Per the cross-cutting spec, each service owns one lifecycle topic:

Service Topic (constant) Source Configuration/Topics.cs
WebSocketManager websocketmanager.connectionstatus virtufin-websocketmanager/src/Virtufin.WebSocketManager/Configuration/Topics.cs:ConnectionStatus
WorkManager workmanager.workerstatus virtufin-workmanager/src/Virtufin.WorkManager/Configuration/Topics.cs:WorkerStatus

The API itself does not own a lifecycle topic — it is the messaging substrate, not a participant. Workers do not own topics either; they receive commands on the topic assigned at deployment time by the WorkManager (e.g. command.websocketmanagercontroller).

Wiring diagram

                          ┌──────────────────────────┐
                          │       virtufin-api       │
                          │  (only Dapr-aware svc)   │
                          └────┬──────────┬──────────┘
                               │          │
                  State.* gRPC │          │ Pubsub.* gRPC
                               │          │
              ┌────────────────┼──────────┼─────────────────┐
              │                │          │                 │
              v                v          v                 v
  ┌────────────────────┐  ┌────────────────────┐  ┌────────────────────┐
  │ virtufin-websocket- │  │ virtufin-work-     │  │  external clients   │
  │     manager         │  │     manager        │  │  (browser, SDKs)    │
  │  - owns: websocket- │  │  - owns: workmgr.  │  │  - talk via api     │
  │    manager.connec-  │  │    workerstatus    │  │    REST/gRPC        │
  │    tionstatus       │  │  - deploys workers │  └────────────────────┘
  └────────────────────┘  └─────────┬──────────┘
                                    │
                                    │ deploys
                                    v
                        ┌────────────────────────┐
                        │   worker processes     │
                        │  (DotNetDLL, Native,   │
                        │   Python, CSharpSrc)   │
                        └────────────────────────┘

Reading the diagram: Solid arrows are gRPC + Dapr-backed calls. The API is the only service that holds Dapr components; the other two services route everything through the API gRPC interface. Workers are spawned by the WorkManager and have no direct Dapr access.

Health-check sequence at startup

Each service has a IHostedService (or equivalent) that gates its readiness probe. Recommended Kubernetes probe config:

readinessProbe:
  httpGet:
    path: /health
    port: 5001
  initialDelaySeconds: 5
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /health/live
    port: 5001
  initialDelaySeconds: 30
  periodSeconds: 30

Service readiness gates:

Service Readiness gates
virtufin-api Dapr sidecar healthy + state store reachable + pubsub broker reachable
virtufin-websocketmanager Dapr sidecar healthy + API gRPC handshake (Health.HealthCheck) + state read/write roundtrip
virtufin-workmanager Dapr sidecar healthy + API gRPC handshake + (optionally) auto-recovery of orphan workers (see WM #15)

The dependency order matters: do not mark a downstream service ready until the API gRPC handshake succeeds. If the API is restarted, downstream services will need to re-handshake; they should fail their liveness probe and the orchestrator will restart them.

Shared Dapr components

All three services share a single Dapr installation and a single set of components. Per the cross-cutting spec §Per-service State Service Names, each service uses its own state service entry but the underlying state store is shared.

Shared components

Component Type Owner Notes
statestore state.redis (Valkey backend) API only — the other two services call State.* gRPCs against the API single backing instance
pubsub pubsub.redis (Valkey streams) API only — same model single backing instance
kubernetes-secrets secretstores.kubernetes API only Dapr resolves keys; services read via DaprClient.GetSecretAsync
daprd (sidecar) per-pod every service each service has its own sidecar with app-protocol: grpc

Per-service components

None. The services do not declare their own Dapr components — they share the API's components via gRPC indirection.

Minimal single-node deployment

A complete docker-compose.yml for a local three-service stack:

services:
  valkey:
    image: valkey/valkey:8
    ports: ["6379:6379"]

  api:
    image: virtufin-api:latest
    environment:
      ASPNETCORE_URLS: http://+:5001
      VIRTUFIN_GRPC_PORT: 5002
      DAPR_GRPC_PORT: 50001
      DAPR_HTTP_PORT: 3500
    ports: ["5001:5001", "5002:5002"]
    depends_on: [valkey]

  websocketmanager:
    image: virtufin-websocketmanager:latest
    environment:
      ASPNETCORE_URLS: http://+:5001
      VIRTUFIN_GRPC_PORT: 5002
      VIRTUFIN_API_HOST: api
      VIRTUFIN_API_GRPC_PORT: 5002
      DAPR_GRPC_PORT: 50001
      DAPR_HTTP_PORT: 3500
    ports: ["5001:5001", "5002:5002"]
    depends_on: [api]

  workmanager:
    image: virtufin-workmanager:latest
    environment:
      ASPNETCORE_URLS: http://+:5001
      VIRTUFIN_GRPC_PORT: 5002
      VIRTUFIN_API_HOST: api
      VIRTUFIN_API_GRPC_PORT: 5002
      DAPR_GRPC_PORT: 50001
      DAPR_HTTP_PORT: 3500
    ports: ["5001:5001", "5002:5002"]
    depends_on: [api]

Each service runs with its own Dapr sidecar — in production use a Dapr control plane install (dapr init -k) rather than the per-pod sidecar injection via daprd.

Minimal Kubernetes deployment

For a single-node Kubernetes deployment (kind, minikube, or a small cluster):

apiVersion: apps/v1
kind: Deployment
metadata: { name: virtufin-api }
spec:
  replicas: 1
  selector: { matchLabels: { app: virtufin-api } }
  template:
    metadata:
      labels: { app: virtufin-api }
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "virtufin-api"
        dapr.io/app-port: "5001"
        dapr.io/app-protocol: "grpc"
        dapr.io/enable-app-health-check: "false"
    spec:
      containers:
      - name: api
        image: virtufin-api:0.0.47
        ports: [{ containerPort: 5001 }, { containerPort: 5002 }]
        env:
        - { name: ASPNETCORE_URLS, value: "http://+:5001" }
        - { name: VIRTUFIN_GRPC_PORT, value: "5002" }
---
apiVersion: v1
kind: Service
metadata: { name: virtufin-api }
spec:
  selector: { app: virtufin-api }
  ports:
  - { name: http, port: 5001, targetPort: 5001 }
  - { name: grpc, port: 5002, targetPort: 5002 }

The WebSocketManager and WorkManager deployments are identical except for app-id and image, with one extra env var:

env:
- { name: VIRTUFIN_API_HOST, value: "virtufin-api" }
- { name: VIRTUFIN_API_GRPC_PORT, value: "5002" }

The dependency chain is enforced by Kubernetes' normal pod scheduling — the API pod just needs to be ready before the downstream services start their gRPC handshake.

Service-to-service gRPC contracts

Caller Callee RPC surface Purpose
WebSocketManager virtufin-api State.*, Pubsub.* connection lifecycle state + events
WorkManager virtufin-api State.*, Pubsub.*, Gateway.* worker registry + gateway calls for worker backends
Worker (via DevKit) virtufin-api Gateway.* service-to-service calls from worker code
Worker (via DevKit) WorkManager (none — WM owns the worker) workers are spawned by WM

All gRPC contracts are exposed via the Virtufin.Api.Client, Virtufin.WebSocketManager.Client, and Virtufin.WorkManager.Client NuGet packages — never call them via raw gRPC stubs unless you're writing a new client in a language that doesn't have a SDK yet.

Verifying the deployment

After all three services report Healthy, run this sanity check from any pod with grpcurl available:

# API health
grpcurl -plaintext virtufin-api:5002 virtufin.api.v1.Health/HealthCheck

# Subscribe WSM to its own topic via API
grpcurl -plaintext virtufin-api:5002 virtufin.api.v1.Pubsub/Subscribe \
  -d '{"topic":"websocketmanager.connectionstatus","subscriber":"test","group":"smoke"}'

# Publish a test event via API (will fan out to WSM)
grpcurl -plaintext virtufin-api:5002 virtufin.api.v1.Pubsub/PublishEvent \
  -d '{"topic":"websocketmanager.connectionstatus","data":"{\"connection_id\":\"smoke\",\"url\":\"ws://localhost\"}","metadata":{"ce-type":"com.virtufin.websocketmanager.connection.connected","ce-source":"urn:test","ce-id":"smoke-1"}}'

If both calls return OK, the wiring is functional. Tear down the smoke subscription afterwards:

grpcurl -plaintext virtufin-api:5002 virtufin.api.v1.Pubsub/Unsubscribe \
  -d '{"topic":"websocketmanager.connectionstatus","subscriber":"test","group":"smoke"}'

See also

  • Getting Started — single-service WM quick start
  • Architecture — WM-specific architecture details
  • DevKit — generic WorkerBase contract for new workers
  • Security — Dapr mTLS, content-source allow-list, audit logging