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.
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
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