StateSet API → StateSet Sync Server (gRPC) Order Dispatch Flow
This document describes a practical architecture for sending orders from the StateSet Rust API (stateset-api) to the StateSet Sync Server (stateset-sync-server) using the Sync Server’s existing gRPC API.
Goal
When an order is created (or updated) instateset-api, reliably dispatch it to stateset-sync-server so the Sync Server can orchestrate downstream integrations (NetSuite, DCL, Cart.com, etc.) via its gRPC surface.
Key Interfaces (as implemented today)
- Sync Server proto:
proto/order_integration.proto(stateset.sync.v1.OrderIntegrationService)- Primary RPC:
SubmitOrder(SubmitOrderRequest) returns (SubmitOrderResponse)
- Primary RPC:
- Sync Server gRPC server:
src/grpc/mod.rs(implementssubmit_order) - Sync Server listen port:
src/server.rs(server.grpc_port, default50051) - Sync Server auth:
x-stateset-api-keymetadata header (src/http/auth.rs) - StateSet API durable trigger: outbox enqueue on order create (
stateset-api/src/services/orders.rs)- Outbox worker:
stateset-api/src/events/outbox.rs - Event loop hook point:
stateset-api/src/events/mod.rs(handle_order_created)
- Outbox worker:
High-Level Flow
- A client creates an order in
stateset-api(HTTP or gRPC). stateset-apicommits the order + items to its DB and enqueues anOrderCreatedoutbox row in the same transaction.- An outbox worker polls for pending events and triggers a Sync dispatch step.
- The dispatcher loads the order + items, builds a
SubmitOrderRequest, and callsstateset.sync.v1.OrderIntegrationService/SubmitOrderon the Sync Server. - The Sync Server validates tenant + API key, then invokes its per-tenant orchestrators to call downstream integrations.
- On success, the outbox row is marked delivered; on retryable failure, it is rescheduled with backoff.
Sequence Diagram
Request Construction (what stateset-api sends)
Minimal viable request (using existing proto)
SubmitOrderRequest requires tenant_id and accepts optional payloads:
stateset_order(CreateOrderPayload) for “StateSet order create” inside the Sync Servernetsuite,dcl,cart(allgoogle.protobuf.Struct) for direct downstream submission
proto/order_integration.proto.
Mapping from stateset-api order model to CreateOrderPayload
stateset-api canonical entities:
- Order:
stateset-api/src/entities/order.rs - Order items:
stateset-api/src/entities/order_item.rs
shopify_order_id(string)order_number(string)created_at(google.protobuf.Timestamp)line_items[]withtitle,quantity,price,sku, etc.
CreateOrderPayload.order_number←orders.order_numberCreateOrderPayload.created_at←orders.created_at(ororder_date, choose one consistently)OrderLineItem.title←order_items.nameOrderLineItem.quantity←order_items.quantity(converti32→f64)OrderLineItem.price←order_items.unit_price(convertDecimal→f64, mind rounding)OrderLineItem.sku←order_items.skuCreateOrderPayload.shopify_order_id:- If you have a true external/channel order id, use it.
- If not, use the
stateset-apiorder UUID string as a stable idempotency key.
stateset_order keys off shopify_order_id (src/orchestrators/order_sync.rs), so this field must be stable for retries.
Calling it by hand
Before wiring the dispatcher, confirm the Sync Server accepts a request from wherestateset-api runs. grpcurl needs the proto because the server does not
serve reflection by default:
Auth & Tenancy
- Set gRPC metadata header
x-stateset-api-key(same key used for the Sync Server’s HTTP API). - Provide
tenant_idin every request; Sync Server resolves tenant configuration and validates the key before executing orchestrators (src/grpc/mod.rs).
Delivery Semantics (reliability)
Recommended: outbox-driven gRPC dispatch
To get “at-least-once” delivery to the Sync Server, perform the gRPC call as part of the outbox processing step and only mark the outbox rowdelivered after the RPC is successful (or after a “non-retryable” error policy decision).
This matches the existing outbox pattern in stateset-api (stateset-api/src/events/outbox.rs) but changes the “delivery target” from “in-process channel send” to “cross-service gRPC”.
Retry policy
- Retry on transport/transient errors (
UNAVAILABLE,DEADLINE_EXCEEDED, timeouts). - Do not retry on permanent validation errors without operator action (
INVALID_ARGUMENT,UNAUTHENTICATED,PERMISSION_DENIED). - Treat application-level partial failures carefully:
SubmitOrdercan returnOKbutsuccess=falsewitherrors[]per integration target (src/grpc/mod.rs).- Decide which targets are “required” vs “best-effort” and only consider the outbox event delivered when your required targets succeeded.
Observability
Suggested minimum telemetry on thestateset-api side for each dispatch attempt:
order_id,tenant_id, targetgrpc_addr- attempt count + backoff delay
- gRPC status code on failure
SubmitOrderResponse.successanderrors[].target/messageon application-level failures
Implementation Notes (code placement)
Instateset-api, the simplest integration point is the existing event processing hook:
- Add a Sync dispatcher call in
stateset-api/src/events/mod.rsinsidehandle_order_created. - For true durability, move the gRPC call into the outbox worker path (
stateset-api/src/events/outbox.rs) so outbox “delivered” reflects remote delivery rather than local enqueue.
Open Questions / Future Improvements
- The current Sync Server gRPC proto is integration-oriented and uses
google.protobuf.Structfor downstream payloads. If you want Sync Server to derive NetSuite/DCL/Cart payloads from the canonicalstateset-apiorder model, add a new RPC/message that accepts a first-class “StateSet order” envelope rather than expecting pre-mapped JSON structs. - TLS: the Sync Server gRPC listener is plaintext by default; deploy behind a TLS-terminating proxy or add tonic TLS configuration if needed.
Next steps
Sync Server API basics
Authentication, tenancy and the HTTP surface this shares a key with.
gRPC flow
What the Sync Server does with the order once SubmitOrder returns.
API contract
The request and response shapes the mapping above targets.
Temporal engine
The alternative to an outbox worker when the dispatch needs to be a durable
workflow in its own right.