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 when the caller opts in.

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 and the request enables change notification
  • THEN a state change event SHALL be published, on the topic determined by the State Change Events requirement in pubsub-topics
  • AND when the request does not enable it, no event SHALL be published — emitting one is opt-in per request, not a property of every state change

Scenario: Bulk state retrieval

  • WHEN a client requests multiple keys in one call
  • THEN the gateway SHALL return the stored value for each key it holds
  • AND the client SHALL supply the keys explicitly: the gateway maintains no registry of a service's keys, so there is no "all state for this service" operation

Scenario: Bulk state deletion with partial failure

  • WHEN a bulk delete succeeds for some keys and fails for others
  • THEN the response SHALL identify the keys that failed, so the caller can retry those rather than repeating the whole batch
  • AND any change event SHALL name only the keys actually deleted

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