Skip to main content

Example: shopify-runtime

shopify-runtime is a metadata Marketplace App for commerce. App id: shopify-runtime. Display name: Shopify. hosting: runtime.

There is no SDK process, no /qefro server, and no Shopify SDK, REST client, GraphQL client, or ShopifyWebhookServer in this package. Credentials never appear in YAML. Shopify does not use the Qefro SDK (SDK remains Focus ERP / Yaaz).

Shopify Marketplace App (metadata only, hosting: runtime)
→ FlowRunner → RuntimeAdapter → Generic HTTP Executor → Shopify Admin API

The HTTP executor is provider-agnostic. The same path runs the http-catalog-runtime example (a non-Shopify catalog fixture). Restaurant Pro and Real Estate keep entity.* storage tools.

Package path: qefro-marketplace-apps/apps/shopify-runtime (collection). Platform CI still vendors a copy under qefro-plugin-platform/docs/examples/shopify-runtime/.

What it proves

A commerce app ships as metadata. Chat tools call the generic HTTP Runtime, not Qefro-managed storage and not a Shopify-specific adapter.

manifest + connections + http_tools + entities + workflows + ui
→ qefro app validate / package / install
→ Connect Shopify (workspace OAuth)
→ Qefro Runtime (FlowRunner → RuntimeAdapter → HTTP executor)

Chat “find black shoes” starts search-products (search_products, execution: http). Customer chat “show my recent orders” starts list-my-orders (list_my_orders). Staff Console “list all orders” starts list-recent-orders (list_orders, staff-only). Host and token come from the workspace connection, never from package URLs or caller parameters.

Contacts stay on the existing Person model (Customer Hub). WhatsApp customers without a Hub email are asked for an address and verify it with a mailed OTP before orders are fetched. {person.email} is server-resolved — never an LLM parameter. See HTTP tools.

Package layout

shopify-runtime/
├── manifest.yaml
├── connections/
│ └── shopify.yaml # declaration + OAuth/webhook metadata — no secrets
├── tools/
│ ├── search_products.yaml
│ ├── list_products.yaml
│ ├── get_product.yaml
│ ├── list_customers.yaml # staff
│ ├── get_customer.yaml # staff
│ ├── list_orders.yaml # staff, shop-wide
│ ├── get_order.yaml # staff
│ ├── list_my_orders.yaml # customer, Hub email + ownership
│ └── get_my_order.yaml # customer, order number + ownership
├── entities/ # staff UI schema (not the chat execution path)
├── workflows/
│ ├── search-products.yaml
│ ├── list-recent-orders.yaml
│ ├── list-my-orders.yaml
│ └── get-my-order.yaml
└── ui/

No src/, no Dockerfile, no Shopify HMAC server.

Connection model

connections/shopify.yaml names the connection, auth header type, OAuth URL templates, scopes, and webhook topic → Business Event mapping. It does not contain base_url, shop domain, or X-Shopify-Access-Token.

Connect Shopify (workspace Tools) runs OAuth in Qefro Runtime:

  1. Admin enters your-store.myshopify.com plus the Shopify app client ID and secret for this workspace.
  2. Runtime stores an unpredictable, short-lived, one-time state bound to the workspace and session (app secret encrypted in that state).
  3. Shopify redirects to /api/v1/integrations/http/oauth/callback.
  4. Callback reads workspace from the state store (never from the browser).
  5. Code is exchanged over HTTPS on the validated shop host (SSRF policy is not weakened). Token is encrypted at rest.
  6. A shop already connected to workspace A cannot be claimed by B.

Client id/secret are per workspace, entered at Connect. They are not process env and not in YAML.

SSRF: the executor blocks localhost, private, and metadata addresses. Allowlist comes from the workspace connection (*.myshopify.com). Redirects are not followed.

Missing OAuth scopes reject tool execution (required_scopes on each HTTP tool). Disconnect revokes the token and deletes credentials.

HTTP tools

ToolSurfaceMethod / pathScope
search_productsallGET /admin/api/2024-10/products.jsonread_products
list_productsallGET /admin/api/2024-10/products.jsonread_products
get_productallGET /admin/api/2024-10/products/{id}.jsonread_products
list_customers / get_customerstaffcustomers.jsonread_customers
list_orders / get_orderstafforders.json (shop-wide)read_orders
list_my_orderscustomerorders.json email={person.email}read_orders
get_my_ordercustomerorders.json name + email={person.email}read_orders

Customer order tools declare identity.require_any: [person.email], collect: email_otp, and ownership on the response. Do not change list_orders to take an email filter — it stays staff shop-wide.

These are generic HTTP tool specs (connection, method, path, access, identity, ownership). There is no if provider == "shopify" in the executor.

SDK-connected apps (Focus ERP / Yaaz) cannot call these tools or steal another workspace's HTTP connection.

Business Flows

id: search-products
steps:
- id: ask_query
type: ask
field: query
- id: list
type: tool
tool: search_products
execution: http
input_map:
query: query
- id: done
type: complete

Restaurant Pro / Real Estate keep execution: runtime + entity.*.

Events and webhooks

Manifest events (order.created, customer.updated, …) are CRM business-event names on the existing bus.

Inbound Shopify webhooks POST to Qefro /webhooks/http/shopify (generic HMAC ingest, not a server in this package). HMAC (X-Shopify-Hmac-Sha256) is verified on the raw body with a small helper outside the package. Invalid/missing signatures are rejected before processing.

Shop domain selects the workspace connection. Topic headers map through YAML metadata onto order.created|updated|cancelled and customer.created|updated. Idempotency uses the Shopify webhook id plus the existing orchestration_events unique key, so CRM Automation runs once. Person mapping uses Customer Hub (email/phone/external identity) — webhook/customer ids are never Person IDs.

Production config

Shopify app credentials are not system-wide. Each workspace Connect form stores client id (plaintext) and client secret (encrypted). HMAC webhooks use that workspace’s secret.

VariablePurpose
PUBLIC_API_URLHTTPS origin for OAuth callback and webhook URL
FRONTEND_URLPortal origin for post-OAuth redirect
ENCRYPTION_KEYEncrypts tokens and the tenant app secret at rest