Skip to main content

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) in stateset-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)
  • Sync Server gRPC server: src/grpc/mod.rs (implements submit_order)
  • Sync Server listen port: src/server.rs (server.grpc_port, default 50051)
  • Sync Server auth: x-stateset-api-key metadata 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)

High-Level Flow

  1. A client creates an order in stateset-api (HTTP or gRPC).
  2. stateset-api commits the order + items to its DB and enqueues an OrderCreated outbox row in the same transaction.
  3. An outbox worker polls for pending events and triggers a Sync dispatch step.
  4. The dispatcher loads the order + items, builds a SubmitOrderRequest, and calls stateset.sync.v1.OrderIntegrationService/SubmitOrder on the Sync Server.
  5. The Sync Server validates tenant + API key, then invokes its per-tenant orchestrators to call downstream integrations.
  6. 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 Server
  • netsuite, dcl, cart (all google.protobuf.Struct) for direct downstream submission
Sync Server proto: 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
Sync Server expects (subset shown):
  • shopify_order_id (string)
  • order_number (string)
  • created_at (google.protobuf.Timestamp)
  • line_items[] with title, quantity, price, sku, etc.
Pragmatic mapping:
  • CreateOrderPayload.order_numberorders.order_number
  • CreateOrderPayload.created_atorders.created_at (or order_date, choose one consistently)
  • OrderLineItem.titleorder_items.name
  • OrderLineItem.quantityorder_items.quantity (convert i32f64)
  • OrderLineItem.priceorder_items.unit_price (convert Decimalf64, mind rounding)
  • OrderLineItem.skuorder_items.sku
  • CreateOrderPayload.shopify_order_id:
    • If you have a true external/channel order id, use it.
    • If not, use the stateset-api order UUID string as a stable idempotency key.
Important: the Sync Server’s existing idempotency logic for stateset_order keys off shopify_order_id (src/orchestrators/order_sync.rs), so this field must be stable for retries.

Auth & Tenancy

  • Set gRPC metadata header x-stateset-api-key (same key used for the Sync Server’s HTTP API).
  • Provide tenant_id in every request; Sync Server resolves tenant configuration and validates the key before executing orchestrators (src/grpc/mod.rs).

Delivery Semantics (reliability)

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 row delivered 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:
    • SubmitOrder can return OK but success=false with errors[] 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 the stateset-api side for each dispatch attempt:
  • order_id, tenant_id, target grpc_addr
  • attempt count + backoff delay
  • gRPC status code on failure
  • SubmitOrderResponse.success and errors[].target/message on application-level failures
Sync Server already logs a “gRPC server listening” line and uses tracing around orchestrator operations; correlate logs via request ids where possible.

Implementation Notes (code placement)

In stateset-api, the simplest integration point is the existing event processing hook:
  • Add a Sync dispatcher call in stateset-api/src/events/mod.rs inside handle_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.Struct for downstream payloads. If you want Sync Server to derive NetSuite/DCL/Cart payloads from the canonical stateset-api order 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.