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
ListMethodsfor 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.getannotation - 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.postannotation 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
PublishEventwith 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) callsPublishEvent - THEN it SHALL have already set
correlationidandreplytopicas 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 ace_timestampvalue - THEN the CloudEvent
timeattribute SHALL be preserved in the published event
Scenario: Default time¶
- WHEN a client publishes a CloudEvent without a
timeattribute - 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
correlationidandreplytopicas 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
correlationidin 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
correlationidextension of kindStringValue - 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
correlationidextension 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/StateRPCs - THEN it SHALL use its own topic (defined in its
Configuration/Topics.cs) and its own state service entry (matching its service name inservices.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-keyheader - 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 --
Unauthenticatedfor 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:Keysconfigured 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:Keysconfigured 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