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.

Calling it by hand

Before wiring the dispatcher, confirm the Sync Server accepts a request from where stateset-api runs. grpcurl needs the proto because the server does not serve reflection by default:
A healthy response carries per-target results rather than a bare acknowledgement:

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.
A gRPC status of OK does not mean the order was integrated. SubmitOrder returns OK whenever the request was well-formed and the tenant authenticated — including when every downstream target failed. A dispatcher that checks only the gRPC status will mark the outbox row delivered and drop the order silently. Check SubmitOrderResponse.success, and inspect errors[] per target, before acknowledging.

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.

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.
Last modified on August 29, 2026