Skip to content

API Gateway

Purpose

Unified API Gateway providing dynamic gRPC service discovery, JSON-to-Protobuf invocation, REST API generation from proto annotations, distributed state management, and pub/sub event streaming for all backend Virtufin services.

Requirements

Requirement: Dynamic Service Discovery

The gateway SHALL discover available backend gRPC services and their methods via gRPC reflection, without requiring pre-compiled proto stubs for each backend.

Scenario: List known services

  • WHEN a client calls ListServices
  • THEN the gateway SHALL return all services defined in its configuration

Scenario: List methods for a service

  • WHEN a client calls ListMethods for a configured service
  • THEN the gateway SHALL return all gRPC methods discovered via reflection from that backend

Scenario: Schema introspection

  • WHEN a client requests a method's input/output schema
  • THEN the gateway SHALL return the full Protobuf message descriptor including field names, types, and oneof groups

Scenario: Reflection cache invalidation

  • WHEN a backend service is redeployed with new methods
  • THEN the gateway SHALL support invalidating its reflection cache for that service

Requirement: Dynamic gRPC Invocation

The gateway SHALL invoke gRPC methods on backend services without pre-compiled client stubs, using the method name and JSON payload provided at runtime.

Scenario: JSON-to-Protobuf invocation

  • WHEN a client sends a JSON payload with a service name and method name
  • THEN the gateway SHALL dynamically marshal the JSON into the method's Protobuf input type, invoke the backend, and return the Protobuf output as JSON

Scenario: Raw bytes invocation

  • WHEN a client sends a raw Protobuf-encoded byte payload
  • THEN the gateway SHALL forward it directly to the backend method and return the raw response bytes

Requirement: REST API from Proto Annotations

The gateway SHALL expose REST/HTTP endpoints equivalent to each gRPC method that has google.api.http annotations. gRPC-Web SHALL be enabled on all endpoints.

Scenario: GET endpoint

  • WHEN a gRPC method defines a google.api.http.get annotation
  • THEN the gateway SHALL serve that endpoint as an HTTP GET on the annotated path

Scenario: POST endpoint

  • WHEN a gRPC method defines a google.api.http.post annotation with a body field
  • THEN the gateway SHALL serve that endpoint as an HTTP POST, mapping the JSON body to the Protobuf field

Scenario: gRPC-Web browser access

  • WHEN a browser client connects via gRPC-Web
  • THEN the gateway SHALL upgrade the request and forward it as native gRPC to the backend

Requirement: Distributed State Management

The gateway SHALL provide CRUD operations on the Dapr state store with optimistic concurrency control via ETags. State changes SHALL emit events.

Scenario: Save state with ETag

  • WHEN a client saves state with an ETag that matches the stored value
  • THEN the save SHALL succeed and the new ETag SHALL be returned

Scenario: Save state with stale ETag

  • WHEN a client saves state with an ETag that does not match the stored value
  • THEN the save SHALL fail with a concurrency conflict error

Scenario: State change events

  • WHEN state is saved or deleted for a registered key
  • THEN a state change event SHALL be published to Dapr pub/sub on a well-known topic

Scenario: Bulk state retrieval

  • WHEN a client requests all state for a registered service
  • THEN the gateway SHALL return all stored key-value pairs

Requirement: Pub/Sub and Event Streaming

The gateway SHALL support publishing events to Dapr pub/sub topics and subscribing clients to topic events via server-side streaming. Dead subscriptions SHALL be cleaned up.

Scenario: Publish event

  • WHEN a client publishes an event to a topic
  • THEN the gateway SHALL forward it to Dapr pub/sub on that topic

Scenario: Subscribe to events

  • WHEN a client opens a server-side streaming subscription to a topic
  • THEN all events published to that topic SHALL be streamed to the client until the subscription is cancelled

Scenario: Dead subscription cleanup

  • WHEN a subscriber's connection drops without explicit unsubscribe
  • THEN the gateway SHALL detect the dead subscription and remove it within one minute

Requirement: Service Configuration

The gateway SHALL load per-service configuration from a JSON or YAML file, specifying each backend's gRPC host/port, Dapr app ID, pubsub component, state store, and optional cron jobs.

Scenario: Configuration hot-reload

  • WHEN the services configuration file is updated
  • THEN the gateway SHOULD reload the configuration without restarting

Requirement: API Documentation

The gateway SHALL expose an OpenAPI/Swagger specification describing all REST endpoints.

Scenario: Swagger UI

  • WHEN a developer navigates to /swagger
  • THEN the interactive API documentation SHALL be served

Requirement: PubSub CloudEvent Propagation

PublishEvent SHALL receive a complete io.cloudevents.v1.CloudEvent proto from the caller and publish it as-is to Dapr — the gateway SHALL NOT add, remove, or modify CloudEvent attributes. The caller is responsible for constructing the CloudEvent with all desired attributes (id, source, type, time, etc.).

Scenario: Pure passthrough

  • WHEN a caller invokes PublishEvent with a CloudEvent
  • THEN the gateway SHALL publish it to Dapr exactly as received, without adding, removing, or modifying any attribute

Scenario: Request-reply correlation

  • WHEN a request-reply client (PublishWithResult) calls PublishEvent
  • THEN it SHALL have already set correlationid and replytopic as CloudEvent attributes before the call

Requirement: CloudEvent Time Attribute

The CloudEvent time attribute (RFC 3339) SHALL be carried as a standard CloudEvent attribute in the proto's attributes map with type ce_timestamp — the caller sets it directly on the io.cloudevents.v1.CloudEvent proto, with no metadata indirection; it flows from publisher through Dapr to subscriber as part of the standard CloudEvent payload.

Scenario: Caller-supplied time

  • WHEN a client publishes a CloudEvent with attributes["time"] set to a ce_timestamp value
  • THEN the CloudEvent time attribute SHALL be preserved in the published event

Scenario: Default time

  • WHEN a client publishes a CloudEvent without a time attribute
  • THEN the gateway SHALL NOT add a default — the caller owns the CloudEvent entirely

Scenario: Publish with correlation ID (request-reply)

  • WHEN a client calls PublishWithResult with a CloudEvent
  • THEN the client library SHALL set correlationid and replytopic as CloudEvent string attributes before passing to PublishEvent
  • AND the Subscribe handler SHALL extract the correlation ID from SubscribeResponse.Cloudevent.Attributes["correlationid"]

Scenario: Publish without correlation ID

  • WHEN a client publishes an event without correlationid in metadata
  • THEN the gateway SHALL publish the event normally without the correlation ID key

Requirement: PubSub Metadata Extraction

The Subscribe handler SHALL extract correlation IDs from Dapr TopicMessage.Extensions using Value.StringValue (not Value.ToString()) to avoid JSON-quoted string artifacts. Extracted metadata SHALL be forwarded to gRPC subscribers via BroadcastToTopicAsync.

Scenario: Extract StringValue metadata

  • WHEN a Dapr message arrives with a correlationid extension of kind StringValue
  • THEN the handler SHALL extract the raw string value without surrounding JSON quotes and include it in the gRPC subscriber broadcast metadata

Scenario: Non-string metadata kind

  • WHEN a Dapr message arrives with a correlationid extension of a non-StringValue protobuf kind
  • THEN the handler SHALL skip that extension rather than producing a JSON-encoded string

Requirement: Per-service Pubsub and State

The gateway SHALL expose the regular Pubsub (PublishEvent, Subscribe, Unsubscribe) and State (SaveState, GetState, DeleteState, QueryState, GetBulkState, DeleteBulkState) RPCs for general use by any service.

Scenario: Service uses its own topic and state entry

  • WHEN a service calls the Pubsub/State RPCs
  • THEN it SHALL use its own topic (defined in its Configuration/Topics.cs) and its own state service entry (matching its service name in services.json) — not another service's

Requirement: Authentication

The gateway SHALL enforce a shared-secret x-api-key scheme on all surfaces: the native gRPC surface and the HTTP surface (hand-written Minimal API routes, docs, and JSON-transcoded gRPC). Health check paths SHALL be exempt. Authentication is enabled only when at least one key is configured; when no keys are configured, the gateway SHALL treat this as an intentional local-development default in the Development environment, and SHALL surface it loudly (a critical-level log at startup and a failed readiness health check) in any other environment, rather than silently running unauthenticated.

Scenario: Valid key on gRPC

  • WHEN a native gRPC or gRPC-Web call presents a configured x-api-key
  • THEN the call SHALL proceed, attributed in logs to the matching key's name

Scenario: Valid key on HTTP

  • WHEN an HTTP request to a Minimal API route or a JSON-transcoded gRPC route presents a configured x-api-key header
  • THEN the request SHALL proceed

Scenario: Missing or invalid key

  • WHEN a call or request presents no key, or a key that does not match any configured key
  • THEN it SHALL be rejected -- Unauthenticated for gRPC, HTTP 401 for HTTP -- while keys are configured

Scenario: Health checks are exempt

  • WHEN a request targets grpc.health.v1.Health/*, /health, or /healthz
  • THEN it SHALL be served without requiring x-api-key, regardless of whether authentication is otherwise enabled

Scenario: Endpoint classification is not caller-controlled

  • WHEN the HTTP-surface middleware decides whether a request is native gRPC or JSON-transcoded gRPC (and therefore already covered by the gRPC-side interceptor) versus a plain HTTP route
  • THEN that decision SHALL be based on server-resolved routing information from the matched endpoint, not on any client-supplied request header -- a caller-controlled signal (e.g. Content-Type) SHALL NOT be sufficient to bypass authentication on a route that isn't actually gRPC

Scenario: No keys configured in Development

  • WHEN the gateway starts with no ApiAuth:Keys configured and the environment is Development
  • THEN authentication SHALL be disabled and the gateway SHALL start normally -- this is the intended zero-config local-dev default

Scenario: No keys configured outside Development

  • WHEN the gateway starts with no ApiAuth:Keys configured and the environment is not Development
  • THEN authentication SHALL still be disabled (the same zero-config behavior, not a hard failure) -- but the gateway SHALL log a critical-level warning at startup and SHALL fail its readiness health check, so an accidental unauthenticated deployment is loudly visible rather than silent