Shopify Integration
Integration posture
Section titled “Integration posture”Per ADR-002, the rewrite adopts a GraphQL-first stance. Shopify’s REST Admin API is designated as “legacy” since October 2024.
Shopify Admin API is an external EngineeredSystem (IOF) — the primary source of commercial transaction Process Records and MaterialArtifact data.
Authentication
Section titled “Authentication”Two token strategies (same as current system, but consolidated into one client):
| Method | Lifetime | Use case |
|---|---|---|
| Permanent offline token (preferred) | No expiration | All production usage |
| Client Credentials Grant | 24 hours, cached | Fallback if permanent token unavailable |
The token is stored as a Wrangler secret (SHOPIFY_ACCESS_TOKEN), injected via Worker bindings.
Bulk operations
Section titled “Bulk operations”The primary data ingestion primitive for the daily sync ProcedureExecution:
mutation { bulkOperationRunQuery( query: """ { orders(query: "updated_at:>'2026-02-01'") { edges { node { id name createdAt updatedAt totalPriceSet { shopMoney { amount currencyCode } } tags lineItems { edges { node { id product { id } title quantity } } } } } } } """ ) { bulkOperation { id status } userErrors { field message } }}Constraints
Section titled “Constraints”| Constraint | Detail |
|---|---|
| Concurrency | 1 bulk query per shop (API version < 2026-01); 5 per shop (>= 2026-01) |
| Nesting | Max 2 levels of connections |
| Result format | JSONL file, download via URL — stored as IBE in R2 |
| Result retention | 7 days after completion |
| Polling | Required — no webhook notification for completion |
Rate limiting
Section titled “Rate limiting”Shopify uses a cost-based leaky bucket for GraphQL:
- 50 points restored per second
- Bucket capacity: ~1,000 points
- Bulk operations have their own concurrency limits separate from point-based limits
Design implication: Interactive dashboard routes never call Shopify directly. All data comes from PlanetScale Measurement Data, refreshed by background ProcedureExecutions.
Shared client module
Section titled “Shared client module”Replaces the 6 duplicated implementations. The client acts as a boundary between the external EngineeredSystem and BluntDashboard’s internal ontological domain:
interface ShopifyClient { // Bulk operations (ProcedureExecution Steps) bulkQuery(query: string): Promise<{ operationId: string }> pollBulkOperation(id: string): Promise<BulkOpStatus> downloadBulkResults(url: string): AsyncIterable<Record<string, unknown>>
// Single-entity operations (MaterialArtifact queries) getProducts(params?: { sinceId?: string }): AsyncIterable<ShopifyProduct> updateMetafield(ownerId: string, namespace: string, key: string, value: string): Promise<void>
// Utilities withRateLimit<T>(fn: () => Promise<T>): Promise<T>}This client handles:
- Token management (permanent preferred, CCG fallback with caching)
- Rate limit tracking and backoff
- Retry with exponential delay (using queue delay semantics for background ProcedureExecutions)
- Structured logging for all API calls
REST minimization
Section titled “REST minimization”REST is retained only behind the shared client for endpoints without GraphQL equivalents. Each REST call is tracked with metrics and flagged for eventual migration.