Skip to content

Real-World Testing Guide

Version: 1.21
Date: March 8, 2026
Status: Active

This guide walks you through setting up a complete real-world testing environment for Forma3D.Connect, connecting all external services to test the full order-to-shipment flow.


Table of Contents

  1. Overview
  2. Prerequisites
  3. Part 1: Shopify Development Store
  4. Protected Customer Data Access
  5. Part 2: SimplyPrint Setup
  6. Azure DevOps Pipeline Variables (SimplyPrint)
  7. Part 3: Sendcloud Sandbox
  8. Azure DevOps Pipeline Variables (Sendcloud)
  9. Part 4: Product Configuration (Benchy)
  10. Part 4b: Multi-Color Printing (Bambu Lab AMS)
  11. Part 5: Forma3D.Connect Configuration
  12. Part 5b: Webhook and Backfill Configuration
  13. OAuth-First API Strategy
  14. Part 6: End-to-End Testing
  15. Troubleshooting
  16. Part 7: AI-Assisted Development with MCP Servers and CLIs
  17. Part 8: Shopify Theme Deployment
  18. Part 9: Grid Product Configuration in Shopify
  19. Part 10: GridFlock Pipeline — How Custom Grid Orders Are Processed
  20. Part 11: Shopify App Proxy & Storefront Security
  21. Part 12: STL Preview Cache
  22. Part 13: Inventory & Stock Management

Overview

The real-world testing setup connects:

uml diagram

Test Flow (Print-to-Order):

  1. Customer purchases a Benchy (green/white/grey) on Shopify test store
  2. Shopify sends webhook to Forma3D.Connect
  3. Forma3D.Connect checks stock availability (hybrid fulfillment)
  4. Stock available: units consumed from shelf, line item marked complete immediately
  5. Partial stock: some units from shelf, remaining printed on demand
  6. No stock: all units printed on demand (original behavior)
  7. Forma3D.Connect creates print job in SimplyPrint for remaining units
  8. When print completes, Forma3D.Connect creates shipping label via Sendcloud
  9. Order is fulfilled in Shopify with tracking number

Stock Replenishment: Products configured with minimumStock > 0 are proactively pre-printed during off-peak hours. See Part 13.


Prerequisites

Service Account Type Purpose
Shopify Partners Free Development store access
SimplyPrint Pro or Team Real print farm connection
Sendcloud Free Trial Sandbox shipping labels

You'll also need:

  • Forma3D.Connect staging environment deployed
  • Access to Azure DevOps variable groups
  • 3D printer connected to SimplyPrint (recommended for full end-to-end testing)

Part 1: Shopify Development Store

1.1 Create a Shopify Partners Account

  1. Go to partners.shopify.com
  2. Sign up for a free Shopify Partners account
  3. Complete the partner registration

1.2 Create a Development Store

  1. In the Partners Dashboard, click StoresAdd store
  2. Select Development store
  3. Configure:
  4. Store name: forma3d-test (or similar)
  5. Store purpose: Select Create a store to test and build
  6. Development store login: Your email and password
  7. Click Save

Your store URL will be: forma3d-test.myshopify.com

1.3 Create a Custom App (for API Access)

Note (January 2026): Shopify has transitioned to a new Dev Dashboard for app development. Legacy custom apps are no longer available for merchants as of January 1, 2026. For development stores created by Partners, you can still use either approach.

Which option should you choose?

Use Case Recommended Option Production Ready?
Testing with production parity Option A: OAuth - Same auth flow as production ✅ Yes
Quick testing only (temporary) Option B: Legacy Custom App ❌ No (deprecated Jan 2026)

Recommendation: Use Option A (OAuth) to test with the same authentication method that production merchant stores will require. This ensures your production deployment will work identically.

OAuth is the only authentication method that works for production merchant stores (as of January 2026). Using OAuth for testing ensures production parity.

Status: ✅ Implemented

OAuth support is now fully implemented in Forma3D.Connect (see ADR-048). Features include:

  • OAuth 2.0 authorization code flow
  • Secure token storage in database (AES-256-GCM encrypted)
  • Automatic token refresh for expiring offline access tokens (24-hour threshold)
  • Backward compatibility with legacy static tokens

Setup steps:

  1. Create an app in the Dev Dashboard at dev.shopify.com
  2. Configure the app version:
  3. Go to Versions in the left sidebar
  4. Click on the active version or create a new version
  5. In the Access section, set Redirect URLs: https://staging-connect-api.forma3d.be/shopify/oauth/callback
  6. In the Access section, set Scopes (see Managing Scopes below)
  7. In the URLs section, set App URL: https://staging-connect.forma3d.be
  8. Set App name: Forma3D Connect
  9. Click Release to publish the version
  10. Go to Settings in the left sidebar and note:
  11. Client ID (API key)
  12. Client secret (API secret) - click the eye icon to reveal
  13. Configure environment variables:
    SHOPIFY_API_KEY=<client-id>
    SHOPIFY_API_SECRET=<client-secret>
    SHOPIFY_APP_URL=https://staging-connect-api.forma3d.be
    SHOPIFY_SCOPES=read_orders,write_orders,read_products,write_products,read_fulfillments,write_fulfillments,read_inventory,read_merchant_managed_fulfillment_orders,write_merchant_managed_fulfillment_orders
    SHOPIFY_TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 32)
    
  14. Deploy the updated API with OAuth endpoints
  15. Navigate to: https://staging-connect-api.forma3d.be/shopify/oauth/authorize?shop=forma3d-dev.myshopify.com&tenantId=YOUR_TENANT_ID
  16. Authorize the app in Shopify's consent screen
  17. You'll be redirected to the Integrations page showing success (or error if something went wrong)
  18. Token is automatically stored in database - no manual copying needed

Managing Shopify App Scopes

Shopify app scopes must be configured in five places that must all stay in sync:

# Location What it controls Where to edit
1 shopify.app.toml (code) Source of truth for Shopify CLI; used to deploy app versions to the Dev Dashboard shopify.app.toml in repo root
2 Shopify Dev Dashboard (app-level) Defines which scopes the app is allowed to request. Result of running shopify app deploy dev.shopify.com > Forma3D Connect > Versions
3 Azure DevOps pipeline variables SHOPIFY_SCOPES variable passed to the deployment; used at runtime Azure DevOps > Pipelines > Library > Variable Groups
4 deployment/staging/env.staging.template Template for manual/direct staging deployments deployment/staging/env.staging.template
5 apps/order-service/src/config/configuration.ts Code-level fallback default when SHOPIFY_SCOPES env var is not set configuration.ts default scopes string

Critical: If a scope is in SHOPIFY_SCOPES but NOT in the Dev Dashboard app version, Shopify will silently ignore it during OAuth. The token will be issued without that scope, and API calls requiring it will fail.

Note: deployment/staging/docker-compose.yml also has a fallback default for SHOPIFY_SCOPES. Keep it in sync when updating scopes.

Complete checklist to add new scopes:

  • Step 1 — Code: Update shopify.app.toml in the project root:
    [access_scopes]
    scopes = "read_fulfillments,write_fulfillments,...,write_draft_orders"
    
  • Step 2 — Code: Update the default scopes string in apps/order-service/src/config/configuration.ts (the SHOPIFY_SCOPES fallback)
  • Step 3 — Code: Update deployment/staging/env.staging.template and deployment/staging/docker-compose.yml fallback defaults
  • Step 4 — Dev Dashboard: Deploy a new app version using the Shopify CLI:
    shopify auth login        # Login if not already authenticated
    shopify app deploy        # Creates a new version (e.g., forma3d-connect-5)
    
  • Step 5 — Dev Dashboard: Verify the new version is Active at dev.shopify.com > Forma3D Connect > Versions
  • Step 6 — Azure DevOps: Update the SHOPIFY_SCOPES variable in the Azure DevOps pipeline variable group to include the new scope
  • Step 7 — Deploy: Push your code changes and let the pipeline deploy (or manually restart the API container on the server)
  • Step 8 — Re-authorize: Reconnect the Shopify store in the Forma3D.Connect Integrations page (disconnect, then reconnect) so the OAuth flow requests the new scopes
  • Step 9 — Verify: Check the granted scopes in the database:
    SELECT scopes FROM "forma3d_connect"."ShopifyShop"
    WHERE "shopDomain" = 'forma3d-dev.myshopify.com' AND "isActive" = true;
    

Current required scopes (as of February 2026):

Scope Purpose
read_orders, write_orders Read and create orders
read_products, write_products Read and manage products
read_fulfillments, write_fulfillments Create fulfillments (mark orders as shipped)
read_inventory Read inventory levels
read_merchant_managed_fulfillment_orders Read fulfillment orders for merchant-managed locations
write_merchant_managed_fulfillment_orders Create fulfillments via the Fulfillment Orders API
write_draft_orders Create draft orders (used by the grid configurator checkout)

Why fulfillment order scopes? Shopify deprecated the legacy POST /orders/{id}/fulfillments.json endpoint. The current Fulfillment Orders API requires fetching fulfillment orders first (GET /orders/{id}/fulfillment_orders.json) then creating a fulfillment (POST /fulfillments.json). These endpoints require the *_merchant_managed_fulfillment_orders scopes in addition to *_fulfillments.

Verifying OAuth Token in Database (pgAdmin):

After completing the OAuth flow, you can verify the token was stored correctly using pgAdmin:

  1. Open pgAdmin and connect to your staging database
  2. Run the following SQL query:
-- Check for stored Shopify OAuth tokens
SELECT
    id,
    "tenantId",
    "shopDomain",
    "tokenType",
    scopes,
    "isActive",
    "installedAt",
    "expiresAt",
    "createdAt",
    "updatedAt",
    -- Token is encrypted, so only showing if it exists
    CASE WHEN "accessToken" IS NOT NULL THEN 'encrypted (present)' ELSE 'missing' END AS token_status
FROM "forma3d_connect"."ShopifyShop"
WHERE "shopDomain" = 'forma3d-dev.myshopify.com'
ORDER BY "createdAt" DESC;

Expected Result:

Column Expected Value
shopDomain forma3d-dev.myshopify.com
tokenType offline
isActive true
token_status encrypted (present)
scopes Array of requested scopes

Note: The actual accessToken value is AES-256-GCM encrypted, so you'll only see encrypted bytes in the database. The token_status column tells you if a token was successfully stored.

Configuring Protected Customer Data Access (Required for Orders API)

As of 2024, Shopify requires apps to explicitly request access to protected customer data (including order data with customer information). Without this, API calls to /orders.json will return 403 Forbidden.

Step 1: Navigate to Partner Dashboard

  1. Go to partners.shopify.com
  2. Find your app Forma3D Connect and click on it
  3. Click API access requests in the left sidebar

Step 2: Configure Protected Customer Data

  1. In the "Protected customer data access" section, click Select next to "Protected customer data"
  2. Under "Protected customer fields (optional)", select the fields needed for order fulfillment:
  3. Name → Click "Select" (required for shipping labels)
  4. Email → Click "Select" (required for order notifications)
  5. Address → Click "Select" (required for shipping)
  6. Phone → Optional but recommended for delivery issues
  7. Scroll down to "Provide your data protection details" and click Provide details
  8. Answer the questions about how your app uses customer data

Step 3: For Development Stores (No Review Required)

Important: If you're using a development store (created via Partners Dashboard), you don't need to submit for formal review. Simply selecting the data access options in Step 2 is sufficient.

The info banner on the page confirms: "Only apps that are distributed on the App Store need to submit their access for review. If you're installing on a dev store, select your data use in step 1 to access protected customer data."

Step 4: Verify Access

After configuring, test the API directly:

# Test with your OAuth token
curl -s -w '\n%{http_code}' \
  -X GET 'https://your-store.myshopify.com/admin/api/2026-01/orders.json?limit=1' \
  -H 'X-Shopify-Access-Token: YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json'

# Expected: 200 with order data
# If 403: Protected customer data access not configured

Common Errors:

Error Cause Solution
403 Forbidden with "This app is not approved to access REST endpoints with protected customer data" Protected customer data not configured Follow steps above in Partner Dashboard
401 Unauthorized with "Invalid API key or access token" Token is invalid or expired Re-authenticate via OAuth flow
403 Forbidden with "access_denied" Missing required scopes Update app scopes and re-install
403 Forbidden with "The api_client does not have the required permission(s)" on fulfillment endpoints Missing *_merchant_managed_fulfillment_orders scopes Add scopes to shopify.app.toml, deploy new app version via shopify app deploy, update SHOPIFY_SCOPES env var, restart API, and reconnect store (see Managing Scopes)

Troubleshooting: "No stores connected" after successful OAuth

If the OAuth completed successfully but the Integrations page shows "No stores connected", there may be a tenant ID mismatch. This can happen if you navigated directly to the OAuth URL instead of using the "Connect Shopify Store" button.

  1. Check your user's tenant ID:
SELECT id, email, "tenantId" FROM "forma3d_connect"."User" WHERE email = 'your-email@example.com';
  1. Check the ShopifyShop tenant ID:
SELECT id, "tenantId", "shopDomain" FROM "forma3d_connect"."ShopifyShop"
WHERE "shopDomain" = 'forma3d-dev.myshopify.com';
  1. If the tenant IDs don't match, update the ShopifyShop record:
    UPDATE "forma3d_connect"."ShopifyShop"
    SET "tenantId" = 'your-actual-tenant-id-from-step-1'
    WHERE "shopDomain" = 'forma3d-dev.myshopify.com';
    

To prevent this issue, always start the OAuth flow from the Integrations page while logged in.

Note: As of January 1, 2026, merchants can no longer create new legacy custom apps. However, Partners can still create them for development stores. This is the recommended approach for testing Forma3D Connect.

Development vs Production:

Environment Approach Why
Development stores (testing) Legacy Custom App ✓ Partner-owned stores keep working; provides static shpat_ token
Production stores (merchants) OAuth integration required Legacy apps are disabled when store transfers to merchant

For this testing guide, we use a legacy custom app because:

  • Your development store (forma3d-dev) is Partner-owned and won't be transferred
  • It provides a simple static token without implementing OAuth
  • Perfect for testing the integration before building production OAuth flow

For production deployment to real merchant stores, Forma3D Connect would need to implement OAuth authentication (see Shopify OAuth documentation).

Step 1: Enable Legacy Custom App Development

  1. Go to your development store admin: https://forma3d-dev.myshopify.com/admin
  2. Navigate to SettingsApps and sales channels
  3. Click Develop apps (top right)
  4. If you see "Allow custom app development", click it → Allow
  5. This enables the legacy custom app option

Step 2: Create the App

  1. Under "Create a legacy custom app" section, click Create a legacy custom app
  2. Enter app name: Forma3D Connect
  3. Select App developer (your email)
  4. Click Create app

Step 3: Configure API Scopes

  1. In the app settings, click Configure Admin API scopes
  2. Enable the following scopes:
  3. read_orders, write_orders
  4. read_products, write_products
  5. read_fulfillments, write_fulfillments
  6. read_inventory
  7. read_merchant_managed_fulfillment_orders, write_merchant_managed_fulfillment_orders
  8. Click Save

Step 4: Install and Get Credentials

  1. Click Install app (you may need to go to the API credentials tab first)
  2. Confirm the installation in the popup
  3. After installation, you'll see the API credentials page with:
Credential Location Format
Admin API access token Revealed once after install - click "Reveal token once" shpat_xxxxxxxx
API key Always visible on the API credentials page xxxxxxxxxxxxxxxx
API secret key Click "Show" to reveal shpat_xxxxxxxx

Important: The Admin API access token is only shown once immediately after installation. Copy it immediately and store it securely (e.g., in a password manager or Azure DevOps variable group). If you miss it, you'll need to uninstall and reinstall the app.

1.4 Webhook Registration (Automatic)

Webhooks are registered automatically via the Shopify API — no manual configuration in the Shopify admin is required. The following webhooks are registered programmatically after OAuth app installation:

Webhook Topic Endpoint URL (staging)
orders/create https://staging-connect-api.forma3d.be/api/v1/webhooks/shopify
orders/updated https://staging-connect-api.forma3d.be/api/v1/webhooks/shopify
orders/cancelled https://staging-connect-api.forma3d.be/api/v1/webhooks/shopify
orders/fulfilled https://staging-connect-api.forma3d.be/api/v1/webhooks/shopify

Important — API-registered vs Admin-registered webhooks: Webhooks created via the Shopify REST API (as we do) are not visible in the Shopify Admin UI (Settings → Notifications → Webhooks). Only webhooks created manually through the admin panel show up there. This is standard Shopify behavior. To list or manage API-registered webhooks, use the Shopify API:

# List all API-registered webhooks for a shop
curl -s https://{shop}.myshopify.com/admin/api/2026-01/webhooks.json \
  -H "X-Shopify-Access-Token: {token}" | jq '.webhooks[] | {id, topic, address}'

Do not create webhooks manually in the Shopify admin — this leads to duplicate deliveries and uses a different signing secret than the one configured in SHOPIFY_WEBHOOK_SECRET.

Multi-tenancy note: This API-based approach is essential for automated onboarding. When full multi-tenancy is implemented, webhook registration should happen automatically as part of the OAuth callback flow (after token exchange), eliminating any manual setup per tenant.

1.5 Enable Test Mode / Bogus Gateway

For test purchases without real credit cards (in the Store Admin):

  1. In the Store Admin (forma3d-dev.myshopify.com/admin), go to SettingsPayments
  2. Scroll to Shopify PaymentsManage
  3. Enable Test mode (toggle at the top)

Option A: Shopify Payments Test Mode (Recommended)

Use these test credit card numbers:

Card Type Number Expiry CVV
Visa (success) 4242 4242 4242 4242 Any future date Any 3 digits
Visa (decline) 4000 0000 0000 0002 Any future date Any 3 digits
Mastercard 5555 5555 5555 4444 Any future date Any 3 digits

Option B: Bogus Gateway (Alternative)

The Bogus Gateway is a simpler test payment provider. To enable it:

  1. Go to SettingsPaymentsAdd payment methods
  2. Search for (for testing) Bogus Gateway and activate it
  3. At checkout, select "Bogus Gateway" as payment method
Field Value for Success Value for Failure
Card number 1 2
Cardholder name Bogus Gateway Bogus Gateway
Expiry Any future date Any future date
CVV 111 111

Tip: Use an email ending in @example.com (e.g., test@example.com) to flag orders as test orders.


Part 2: SimplyPrint Setup

2.1 Create/Access SimplyPrint Account

  1. Go to simplyprint.io
  2. Create an account or log in to your existing account
  3. Ensure you have a Pro or Team plan for API access

2.2 Get API Credentials

  1. Go to SettingsAPI (or Developer Settings)
  2. Generate an API Key
  3. Note your Company ID (format: S123456 or numeric)

2.3 Connect Your Printer(s)

Option A: Connect a Real Printer (Recommended)

  1. Go to simplyprint.io/setup-guide
  2. Select your printer brand and model
  3. Follow the setup instructions:
  4. Smart Printers (Creality K1, etc.): Install SimplyPrint directly
  5. Bambu Lab printers: Requires local bridge device (guide)
  6. Traditional printers (Ender-3, etc.): Use Raspberry Pi with OctoPrint
  7. Use the setup code from SimplyPrint to link your printer

Option B: Test Without a Physical Printer

SimplyPrint does not have a built-in virtual printer feature. For testing without a physical printer, you have these alternatives:

  1. Mock the SimplyPrint API responses - Use the existing test mocks in the codebase
  2. Use SimplyPrint's API in "dry run" mode - Create queue items without actually printing
  3. Set SIMPLYPRINT_POLLING_ENABLED=false - Disable polling and manually update job statuses via the API

For end-to-end testing, a real printer connection is recommended. You can use an inexpensive printer like an Ender-3 with a Raspberry Pi running OctoPrint.

2.4 Upload Benchy Files

You need to upload 3 versions of the Benchy STL (one for each color):

  1. Download the Benchy model from MakerWorld
  2. Go to Files in SimplyPrint
  3. Create a folder: benchy-variants
  4. Upload the STL file 3 times with distinct names:
  5. benchy-green.stl (or pre-sliced .gcode/.3mf)
  6. benchy-white.stl
  7. benchy-grey.stl

  8. Note down the File ID for each file:

  9. Click on each file → look at the URL or file details
  10. File IDs are used in product mappings

Tip: If using pre-sliced files, ensure each uses the correct filament color profile.

2.5 Configure Webhooks (Required for Terminal States)

SimplyPrint webhooks are the primary mechanism for detecting terminal states (COMPLETED, FAILED, CANCELLED). When webhooks are missed, the reconciliation service automatically detects terminal states via SimplyPrint's print history API (GET /{id}/jobs/Get) after a 5-minute grace period. This self-healing mechanism eliminates the need for manual forceStatus intervention in routine operations.

Note: If the Forma3D.Connect API is down when a print completes or fails and the webhook is missed, the reconciliation service will detect the terminal state within ~6 minutes of coming back online (1 min reconciliation cycle + 5 min grace period). The forceStatus admin endpoint remains available as an ultimate fallback for edge cases.

Setup steps:

  1. Go to SettingsWebhooks (or IntegrationsWebhooks)
  2. Add webhook URL: https://staging-connect-api.forma3d.be/webhooks/simplyprint
  3. Select events:
  4. Job Started
  5. Job Done
  6. Job Failed
  7. Job Cancelled
  8. Generate a webhook secret — SimplyPrint does not provide one automatically. Generate your own:
    openssl rand -hex 32
    
    Copy the output (e.g., a1b2c3d4e5f6...) and configure it in both SimplyPrint and your environment variables.

2.6 Azure DevOps Pipeline Variables

After obtaining the credentials above, set these variables in the forma3d-staging variable group in Azure DevOps:

Variable Where to Find Example Value
SIMPLYPRINT_API_URL Static URL https://api.simplyprint.io
SIMPLYPRINT_API_KEY Settings → API → Generate API Key sp_xxxxxxxxxxxxx
SIMPLYPRINT_COMPANY_ID Settings → API (or visible in URL when logged in) S123456 or 123456
SIMPLYPRINT_WEBHOOK_SECRET Self-generated with openssl rand -hex 32 (see step 2.5) a1b2c3d4e5f6... (64 hex chars)

Note: The full list of environment variables and their usage is documented in Part 5.1.


Part 3: Sendcloud Sandbox

3.1 Create Sendcloud Account

  1. Go to panel.sendcloud.sc/register
  2. Sign up for a free trial account
  3. Complete the registration and email verification

3.2 Test Mode / Avoiding Real Shipping Charges

Sendcloud does not show a visible "Test mode" or "Sandbox" indicator in the dashboard (and panel.sendcloud.sc redirects to app.sendcloud.com). You can still avoid real charges in these ways:

Option A: Free (monthly) subscription

  • On the Dashboard, check Huidig abonnement (Current subscription). If it shows Free (monthly), you have a limited label allowance (e.g. 50/month) and usage is intended for trying the product.
  • Labels created on free accounts may be watermarked or treated as test labels depending on Sendcloud's current policy; check the first label you create.

Option B: Use the "Unstamped letter" shipping method for API testing

  • Sendcloud's API supports a special Unstamped letter shipping method that does not incur charges. Use it when calling the API to create parcels for testing.
  • Get its current id from: GET https://panel.sendcloud.sc/api/v2/shipping_methods (or your usual Sendcloud API base URL). In the response, find the method named "Unstamped letter" and use that id as DEFAULT_SHIPPING_METHOD_ID for testing only.
  • See Sendcloud: Creating test labels.

Option C: Create labels and cancel within deadline

  • For other shipping methods, you can create a label and then cancel it before the carrier's cancellation deadline so you are not charged. Not all carriers support cancellation — check Sendcloud help.

How to confirm you're not being charged:

  • Create one label (via the app or Forma3D.Connect), then in Sendcloud check the label PDF for a "TEST" watermark and your account/billing for any charges.
  • For full safety when testing the API, use Unstamped letter (Option B) or keep the Free plan and stay within the monthly label limit.

3.3 Get API Credentials

Forma3D.Connect uses direct API integration (not a pre-built e-commerce connector), so you need to create a Sendcloud API integration.

  1. Go to SettingsIntegrations
  2. In the integrations marketplace, find and click "Sendcloud API"
  3. This is the correct option for direct API access
  4. Do NOT select platform-specific integrations (Shopify, WooCommerce, etc.) — Forma3D.Connect handles those integrations itself
  5. Click Create new integration (or view existing)
  6. Note down:
  7. Public Key
  8. Secret Key
  9. Configure the webhook (while you're here):
  10. Enable Webhook feedback enabled
  11. Set Webhook URL: https://staging-connect-api.forma3d.be/webhooks/sendcloud
  12. Set a Webhook Signature Key (min 16 chars, must include uppercase, lowercase, numbers, and special characters)
  13. Click Test API Webhook to verify connectivity
  14. Save the configuration

Note: See Part 5b.4 for detailed webhook configuration and environment variable setup.

3.4 Configure Sender Address

  1. Go to SettingsAddressesSender addresses
  2. Add your test sender address:
  3. Company: Forma3D Test
  4. Address: Your test address
  5. City, Postal Code, Country
  6. Note the Sender Address ID (visible in URL or settings)

3.5 Select Default Shipping Method

The "Default Shipping Method" is not under Settings → Integrations. It is the Shipping Method ID of a carrier you use when creating labels; you choose one and put its ID in DEFAULT_SHIPPING_METHOD_ID.

Where to find it in the Sendcloud dashboard:

  1. In the left sidebar, use the main Verzenden (Ship) section — not Instellingen (Settings) → Integraties (Integrations).
  2. Open Verzenden (Ship), then look for Shipping methods, Carriers, or Vervoermethoden (depending on language and Sendcloud version). Alternatively try Settings → Carriers.
  3. Enable or open a test-friendly carrier (e.g., PostNL, DHL, bpost) and note the Shipping Method ID (often in the URL or method details when you click the method).

Alternative: get the ID via the API

If the UI does not show the ID clearly, call the Sendcloud API (e.g. with your Public/Secret key) and use the returned id:

# Replace SENDER_ADDRESS_ID with your sender address ID from section 3.4
curl -u "PUBLIC_KEY:SECRET_KEY" \
  "https://panel.sendcloud.sc/api/v2/shipping_methods?sender_address=SENDER_ADDRESS_ID"

Pick the id of the method you want (e.g. PostNL Standard) and set that as DEFAULT_SHIPPING_METHOD_ID.

Common shipping method IDs (examples; your account may differ):

Carrier Method ID (example)
PostNL Standard 8
DHL Parcel 2001
bpost Domestic 3001

Note: Exact IDs depend on your account and country. Do not cache them for long; use the dashboard or API to confirm.

3.6 Azure DevOps Pipeline Variables

After obtaining the credentials above, set these variables in the forma3d-staging variable group in Azure DevOps:

Variable Where to Find Example Value
SHIPPING_ENABLED Set to true to enable shipping label creation true
SENDCLOUD_PUBLIC_KEY Settings → Integrations → Sendcloud API → Public Key xxxxxxxxxxxxxxxx
SENDCLOUD_SECRET_KEY Settings → Integrations → Sendcloud API → Secret Key xxxxxxxxxxxxxxxx
DEFAULT_SHIPPING_METHOD_ID Shipping → Shipping methods → Click method → ID in URL/details 8 (PostNL Standard)
DEFAULT_SENDER_ADDRESS_ID Settings → Addresses → Sender addresses → ID in URL/settings 12345
SENDCLOUD_WEBHOOK_SECRET Same as SENDCLOUD_SECRET_KEY (for "Sendcloud API" integration) (same as secret key)

Webhook Signature Verification:

For the "Sendcloud API" integration type, Sendcloud uses your API Secret Key to sign webhooks. Set SENDCLOUD_WEBHOOK_SECRET to the same value as SENDCLOUD_SECRET_KEY.

Note: Other integration types (e.g., Shopify, WooCommerce connectors) may have a separate "Webhook Signature Key" field. For direct API integrations, the Secret Key is used.

Note: The full list of environment variables and their usage is documented in Part 5.1. Webhook configuration details are in Part 5b.4.


Part 4: Product Configuration (Benchy)

4.1 Create Shopify Products

In your Shopify development store:

  1. Go to ProductsAdd product
  2. Create the Benchy product:

Product Details:

  • Title: 3D Benchy Test Boat
  • Description: The classic 3D printing benchmark model
  • Product type: 3D Print
  • Price: €9.99 (or any test price)

Add Variants:

  1. Click Add options like size or color → Add option: Color
  2. Add variant values: Green, White, Grey
  3. For each variant, set a unique SKU:
Variant SKU
Green BENCHY-GREEN-001
White BENCHY-WHITE-001
Grey BENCHY-GREY-001
  1. Add product images (optional but helpful for testing)
  2. Click Save

4.2 Note Product and Variant IDs

After saving, you can find IDs in the Shopify URL or via API:

Product URL: /admin/products/1234567890
Product ID: 1234567890 (or gid://shopify/Product/1234567890)

Variant URL: /admin/products/1234567890/variants/9876543210
Variant ID: 9876543210 (or gid://shopify/ProductVariant/9876543210)

4.3 Product Mapping: Linking Shopify to SimplyPrint

This is the critical step that tells Forma3D.Connect which 3D file to print for each product variant.

Understanding Product Mappings:

uml diagram

Create mappings for each color variant:

You need to create 3 product mappings (one per color variant), each pointing to a different SimplyPrint file.


Part 4b: Multi-Color Printing (Bambu Lab AMS)

If you have Bambu Lab printers with AMS (like the A1 Combo), you can offer multi-color prints. There are several approaches depending on your use case.

For true multi-color prints using AMS, upload pre-sliced .3mf files from Bambu Studio that have color/filament assignments baked in.

uml diagram

Setup Steps:

  1. In Bambu Studio:
  2. Import your model (STL/OBJ/3MF)
  3. Paint or assign colors to different parts using the color painting tool
  4. Configure filament types for each AMS slot
  5. Slice and export as .3mf

  6. In SimplyPrint:

  7. Upload the .3mf file (preserves color assignments)
  8. Ensure your Bambu printer is connected via SimplyPrint
  9. AMS slot assignments are read from the .3mf file

  10. In Shopify:

  11. Create a variant for the multi-color option (e.g., "Rainbow")
  12. SKU: BENCHY-RAINBOW-001

  13. Product Mapping:

    {
      "sku": "BENCHY-RAINBOW-001",
      "productName": "3D Benchy - Rainbow",
      "assemblyParts": [
        {
          "partName": "Multi-color Benchy",
          "simplyPrintFileId": "YOUR_MULTICOLOR_3MF_FILE_ID",
          "simplyPrintFileName": "benchy-multicolor.3mf"
        }
      ]
    }
    

Approach 2: Multiple Color Combinations as Variants

If you want to offer different multi-color combinations, create separate pre-sliced files for each:

Shopify Variant SKU SimplyPrint File
Classic (Blue/White) BENCHY-CLASSIC-001 benchy-blue-white.3mf
Sunset (Orange/Yellow) BENCHY-SUNSET-001 benchy-orange-yellow.3mf
Ocean (Blue/Teal) BENCHY-OCEAN-001 benchy-blue-teal.3mf

Each file contains the same model but with different color assignments in the .3mf.

Approach 3: Assembly Mode (Multi-Part, Single Color Each)

For products made of multiple separately-printed parts (each in one color), use the existing assembly feature:

uml diagram

This creates separate print jobs for each part, which can be printed on different printers or sequentially.

AMS Slot Configuration

For consistent multi-color prints, standardize your AMS slot assignments:

AMS Slot Filament Use Case
Slot 1 White PLA Primary/base color
Slot 2 Black PLA Details/text
Slot 3 Variable Accent color 1
Slot 4 Variable Accent color 2

Important: Your .3mf files must be sliced with the same slot assignments as your physical AMS configuration.

Understanding AMS Slot vs Color Assignment

Key concept: Bambu Studio slices for slots, not colors. The gcode says "use filament from Slot 1", not "use blue filament".

uml diagram

Two behaviors depending on filament type:

Filament Type RFID Auto-Remap? Production Suitability
Bambu-branded Yes Yes, with confirmation prompt Medium (may pause for confirmation)
Third-party No No, uses slot as-is High (if slots are standardized)

Production Print Farm Strategy

For automated, unattended printing (recommended for Forma3D.Connect):

Strategy: Standardize all printers identically

Printer 1 (A1 Combo):          Printer 2 (A1 Combo):
├── Slot 1: White PLA          ├── Slot 1: White PLA
├── Slot 2: Black PLA          ├── Slot 2: Black PLA
├── Slot 3: Green PLA          ├── Slot 3: Green PLA
└── Slot 4: Grey PLA           └── Slot 4: Grey PLA

Then slice ALL your multi-color files with this exact configuration. Any print can go to any printer.

For single-color products (like the Benchy color variants):

You don't need multi-color slicing. Instead:

  • Upload 3 identical .stl files (or the same file 3 times)
  • Each sliced with a single color from Slot 1
  • Physically load the appropriate color in Slot 1 when that color's jobs are queued

Or use printer groups in SimplyPrint:

  • "Green Printer Group" → Printers loaded with green in Slot 1
  • "White Printer Group" → Printers loaded with white in Slot 1
  • Route jobs to the appropriate group based on the ordered color

Offline Slicing Workflow

You can slice without a connected printer:

  1. Open Bambu Studio (no printer connection needed)
  2. Select printer profile: A1 Combo
  3. Configure AMS slots in the filament panel (virtual configuration)
  4. Import and paint your model with colors
  5. Slice → Generates gcode with slot assignments
  6. Export as .3mf (preserves project + gcode + plate info)
  7. Upload to SimplyPrint for later printing

The exported .3mf contains everything needed. When SimplyPrint sends it to your printer, it will use the slot assignments from your slice.

SimplyPrint + Bambu Lab Integration

SimplyPrint connects to Bambu printers via:

  1. Bambu Cloud - Remote connection through Bambu's servers
  2. Local Network - Direct LAN connection (faster, requires same network)

To set up in SimplyPrint:

  1. Go to PrintersAdd Printer
  2. Select Bambu Lab
  3. Enter your Bambu Cloud credentials or local IP
  4. SimplyPrint will detect your A1 Combo and AMS configuration

The defaultPrintProfile in ProductMapping can include AMS-specific settings:

{
  "defaultPrintProfile": {
    "material": "PLA",
    "qualityPreset": "0.20mm Standard",
    "infillPercentage": 15,
    "supportEnabled": false,
    "additionalSettings": {
      "amsEnabled": true,
      "flushingVolume": "medium",
      "primeWaste": "chute"
    }
  }
}

Note: Some settings depend on SimplyPrint's Bambu integration capabilities. Check SimplyPrint documentation for supported parameters.


Part 5: Forma3D.Connect Configuration

5.1 Update Azure DevOps Variable Group

Update the forma3d-staging variable group with your test credentials:

Variable Value
SHOPIFY_SHOP_DOMAIN forma3d-test.myshopify.com
SHOPIFY_API_KEY Your app API key
SHOPIFY_API_SECRET Your app API secret
SHOPIFY_ACCESS_TOKEN shpat_xxxxx (from app installation)
SHOPIFY_WEBHOOK_SECRET Must equal SHOPIFY_API_SECRET (app client secret — used to verify API-registered webhook HMAC)
SHOPIFY_API_VERSION 2026-01
SIMPLYPRINT_API_URL https://api.simplyprint.io
SIMPLYPRINT_API_KEY Your SimplyPrint API key
SIMPLYPRINT_COMPANY_ID Your company ID (e.g., S123456)
SIMPLYPRINT_WEBHOOK_SECRET Your webhook secret (if configured)
SHIPPING_ENABLED true
SENDCLOUD_PUBLIC_KEY Your Sendcloud public key
SENDCLOUD_SECRET_KEY Your Sendcloud secret key
DEFAULT_SHIPPING_METHOD_ID Your chosen shipping method ID
DEFAULT_SENDER_ADDRESS_ID Your sender address ID
STOCK_REPLENISHMENT_ENABLED false (set to true to enable scheduled stock replenishment)
STOCK_REPLENISHMENT_CRON */15 * * * * (replenishment evaluation interval, default: every 15 min)
STOCK_REPLENISHMENT_MAX_CONCURRENT 5 (max concurrent stock print jobs)
STOCK_REPLENISHMENT_ORDER_THRESHOLD 3 (skip replenishment when order queue exceeds this)

5.2 Deploy Configuration

After updating the variable group, trigger a deployment:

# Push a small change to develop branch, or trigger manually in Azure DevOps
git checkout develop
git commit --allow-empty -m "Trigger deployment for config update"
git push

5.3 Verify Connections

After deployment, check the health endpoints:

# Check API health (includes integration status)
curl https://staging-connect-api.forma3d.be/health

# Expected response includes:
# - database: connected
# - simplyprint: connected (if configured correctly)

# Check dependencies specifically
curl https://staging-connect-api.forma3d.be/health/dependencies

5.4 Create Product Mappings via API

Use the Forma3D.Connect API to create product mappings:

# Get your INTERNAL_API_KEY from the variable group
API_KEY="your-internal-api-key"
API_URL="https://staging-connect-api.forma3d.be"

# Create mapping for Green Benchy
curl -X POST "$API_URL/api/v1/product-mappings" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shopifyProductId": "1234567890",
    "shopifyVariantId": "9876543210001",
    "sku": "BENCHY-GREEN-001",
    "productName": "3D Benchy - Green",
    "description": "Green PLA Benchy test print",
    "defaultPrintProfile": {
      "material": "PLA",
      "infillPercentage": 20,
      "layerHeight": 0.2,
      "supportEnabled": false
    }
  }'

# Note the returned mapping ID, then add the assembly part (file reference)
MAPPING_ID="returned-mapping-id"

curl -X POST "$API_URL/api/v1/product-mappings/$MAPPING_ID/parts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "partName": "Benchy Model",
    "partNumber": 1,
    "simplyPrintFileId": "YOUR_GREEN_FILE_ID",
    "simplyPrintFileName": "benchy-green.stl",
    "quantityPerProduct": 1
  }'

Repeat for White and Grey variants with their respective:

  • shopifyVariantId
  • sku
  • simplyPrintFileId

5.5 Alternative: Create Mappings via Dashboard

If the web dashboard is available:

  1. Go to https://staging-connect.forma3d.be
  2. Navigate to MappingsNew Mapping
  3. Fill in the form for each color variant
  4. Add the SimplyPrint file reference in the assembly parts section

Part 5b: Webhook and Backfill Configuration

Forma3D.Connect uses webhooks for real-time updates from Shopify, SimplyPrint, and SendCloud. Additionally, backfill/reconciliation services ensure no data is lost during downtime. This section explains how to configure these features.

5b.1 Architecture Overview

uml diagram

5b.2 Shopify Webhook Setup

Shopify webhooks are registered via the Shopify REST API (not manually in the admin panel). You should also enable the backfill service to catch any orders missed during downtime.

Required Webhooks (registered via API)

Webhook Topic Endpoint Purpose
orders/create /api/v1/webhooks/shopify New order created
orders/updated /api/v1/webhooks/shopify Order status changed
orders/cancelled /api/v1/webhooks/shopify Order cancelled
orders/fulfilled /api/v1/webhooks/shopify Order fulfilled

Important: API-registered webhooks do not appear in the Shopify Admin UI (Settings → Notifications → Webhooks). Use the GET /admin/api/{version}/webhooks.json endpoint to list and manage them. See Section 1.4 for details.

Shopify API Authentication Strategy: OAuth-First

Forma3D.Connect uses an OAuth-first approach for all Shopify API calls:

uml diagram

How it works:

  1. OAuth Mode (Preferred): When shops are connected via OAuth, the system retrieves encrypted access tokens from the ShopifyShop database table. Each shop has its own token, enabling multi-tenant support.

  2. Legacy Mode (Fallback): Only used when NO OAuth shops exist in the database AND SHOPIFY_ACCESS_TOKEN environment variable is set to a valid (non-placeholder) value.

  3. Placeholder Detection: The following values in SHOPIFY_ACCESS_TOKEN are treated as placeholders and disable legacy mode:

  4. TODO, your-, placeholder, xxx, test-, example

Key Implementation Details:

Component OAuth Method Legacy Method
Get orders getOrdersForShop(tenantId, shopDomain, params) getOrders(params)
Get single order getOrderForShop(tenantId, shopDomain, orderId) getOrder(orderId)
Create fulfillment createFulfillmentForShop(tenantId, shopDomain, ...) createFulfillment(orderId, ...)
Backfill service runOAuthBackfillWithShops() runLegacyBackfill()

Code reference (shopify-backfill.service.ts):

// Check if there are OAuth-connected shops first (preferred method)
const oauthShops = await this.shopRepository.findAllActive();

if (oauthShops.length > 0) {
  // OAuth mode: use tokens from database (preferred)
  this.logger.log(`Using OAuth mode for ${oauthShops.length} connected shop(s)`);
  await this.runOAuthBackfillWithShops(oauthShops, result);
} else if (this.shopifyClient.isLegacyMode()) {
  // Legacy fallback: only if no OAuth shops exist
  this.logger.log('No OAuth shops found, falling back to legacy mode');
  await this.runLegacyBackfill(sinceId, result);
} else {
  this.logger.debug('No Shopify integration configured');
}

Shopify Backfill Configuration

The backfill service runs every 5 minutes and uses Shopify's since_id pagination to fetch any orders that might have been missed.

Environment Variables:

Variable Default Description
SHOPIFY_BACKFILL_ENABLED true Enable/disable the backfill service
SHOPIFY_BACKFILL_BATCH_SIZE 50 Number of orders to fetch per batch

Admin Endpoints:

Endpoint Method Purpose
/api/v1/admin/shopify/backfill/status GET Check backfill status and watermark
/api/v1/admin/shopify/backfill/trigger POST Manually trigger backfill
/api/v1/admin/shopify/backfill/reset POST Reset watermark (re-scan all orders)

Example: Check backfill status

curl https://staging-connect-api.forma3d.be/api/v1/admin/shopify/backfill/status \
  -H "Authorization: Bearer $INTERNAL_API_KEY"

# Response:
# {
#   "enabled": true,
#   "isRunning": false,
#   "lastSinceId": 123456789,
#   "lastRunAt": "2026-01-22T10:00:00Z"
# }

5b.3 SimplyPrint Webhook Setup

SimplyPrint sends webhooks for print job status changes. You need to configure the webhook URL in SimplyPrint and set the verification secret.

Step 1: Generate Webhook Secret

SimplyPrint does not provide a webhook secret automatically — you must generate your own:

openssl rand -hex 32

Save this value securely. You'll configure it in both SimplyPrint and your environment variables.

Step 2: Configure Webhook in SimplyPrint

  1. Log into SimplyPrint
  2. Go to SettingsIntegrationsWebhooks
  3. Add a new webhook:
  4. URL: https://staging-connect-api.forma3d.be/webhooks/simplyprint
  5. Secret/Token: Paste the secret you generated in Step 1
  6. Events: Select all job events (job.started, job.done, job.failed, job.cancelled, etc.)

Step 3: Set Environment Variables

Variable Description
SIMPLYPRINT_WEBHOOK_SECRET The secret you generated with openssl rand -hex 32
SIMPLYPRINT_RECONCILIATION_ENABLED Enable job status reconciliation (default: true)

SimplyPrint Reconciliation Service

The reconciliation service runs every 1 minute and checks active print jobs against SimplyPrint's API. Unlike Shopify's backfill (which can recover any missed orders), this only works for in-progress jobs.

What it does:

  1. Finds all print jobs in QUEUED, ASSIGNED, or PRINTING status
  2. Queries SimplyPrint's queue and printers for current status
  3. Emits status change events for any discrepancies

Critical Limitation: The reconciliation service cannot detect COMPLETED or FAILED jobs from the API alone — SimplyPrint does not expose job history. If you miss a webhook for a terminal state (e.g., the API was down when a print finished), the job will remain stuck in its last known state. There is no automatic recovery for missed terminal state webhooks.

5b.4 SendCloud Webhook Setup

SendCloud webhooks provide real-time shipment status updates (in transit, delivered, failed, etc.). These statuses are surfaced on every order API response via the shipmentStatus field and can be used to filter orders (e.g., GET /api/v1/orders?shipmentStatus=IN_TRANSIT or GET /api/v1/orders?readyToShip=true). The dashboard orders list shows shipping status badges alongside order status badges.

Step 1: Configure Webhook in SendCloud

  1. Log into SendCloud Panel
  2. Go to SettingsIntegrationsConfigure (your Sendcloud API integration)
  3. Enable Webhook feedback enabled (checkbox)
  4. Set Webhook URL: https://staging-connect-api.forma3d.be/webhooks/sendcloud
  5. Click Test API Webhook to verify connectivity
  6. Click Opslaan (Save) to save the configuration

Step 2: Set Environment Variables

Variable Description
SENDCLOUD_WEBHOOK_SECRET Your Sendcloud API Secret Key (same as SENDCLOUD_SECRET_KEY)
SENDCLOUD_RECONCILIATION_ENABLED Enable shipment status reconciliation (default: true)

How webhook signing works: For "Sendcloud API" integrations, Sendcloud signs webhooks using HMAC-SHA256 with your API Secret Key. This means SENDCLOUD_WEBHOOK_SECRET should be set to the same value as SENDCLOUD_SECRET_KEY. See the Sendcloud webhook documentation for details.

SendCloud Status Mapping

SendCloud uses numeric status IDs. Forma3D.Connect maps these to our shipment statuses:

SendCloud Status ID Shipment Status Description
1-10 LABEL_CREATED Label created, ready to ship
11-99 ANNOUNCED Handed to carrier
1000-1098 IN_TRANSIT Package in transit
1100-1199 CANCELLED Shipment cancelled
1999 FAILED Delivery attempt failed
2000 DELIVERED Successfully delivered
2001+ FAILED Returned/unable to deliver

SendCloud Reconciliation Service

The reconciliation service runs every 5 minutes and polls SendCloud for status updates on all active shipments.

What it does:

  1. Finds all shipments in PENDING, LABEL_CREATED, ANNOUNCED, or IN_TRANSIT status
  2. Queries SendCloud's API for current parcel status
  3. Updates shipment status if it differs from local state

5b.5 Webhook Idempotency

All webhooks use database-backed idempotency to prevent duplicate processing:

  • Shopify: Uses X-Shopify-Webhook-Id header
  • SimplyPrint: Uses webhook_id from payload
  • SendCloud: Uses combination of parcel_id + status_id + timestamp

Idempotency records are retained for 24 hours by default (configurable via WEBHOOK_IDEMPOTENCY_RETENTION_HOURS).

5b.6 Verifying Webhook Configuration

Test Shopify Webhooks

# In Shopify admin, go to Settings → Notifications → Webhooks
# Click "Send test notification" for orders/create

# Check Forma3D.Connect logs
ssh root@167.172.45.47
docker compose logs -f api | grep -i shopify

Test SimplyPrint Webhooks

# Start a test print job in SimplyPrint
# Watch for webhook events in logs

docker compose logs -f api | grep -i simplyprint

Test SendCloud Webhooks

# In SendCloud panel, use the "Test API Webhook" button
# Check logs for webhook receipt

docker compose logs -f api | grep -i sendcloud

# Verify with status endpoint
curl https://staging-connect-api.forma3d.be/api/v1/orders/ORDER_ID \
  -H "Authorization: Bearer $API_KEY" | jq '.shipment'

5b.7 Monitoring Backfill Services

Check backfill/reconciliation status via health endpoint:

curl https://staging-connect-api.forma3d.be/health

# Response includes backfill status information

Or check event logs for reconciliation activity:

curl "https://staging-connect-api.forma3d.be/api/v1/event-logs?eventType=system.shopify_backfill" \
  -H "Authorization: Bearer $API_KEY"

curl "https://staging-connect-api.forma3d.be/api/v1/event-logs?eventType=system.simplyprint_reconciliation" \
  -H "Authorization: Bearer $API_KEY"

curl "https://staging-connect-api.forma3d.be/api/v1/event-logs?eventType=system.sendcloud_reconciliation" \
  -H "Authorization: Bearer $API_KEY"

Part 6: End-to-End Testing

6.1 Test Flow Checklist

  • All product mappings created
  • SimplyPrint files uploaded and IDs noted
  • Webhooks configured and verified
  • Health endpoints showing connected status
  • Stock management configured (optional — see Part 13)

6.2 Place a Test Order

  1. Open your Shopify test store: https://forma3d-test.myshopify.com
  2. Add the 3D Benchy Test Boat - Green to cart
  3. Proceed to checkout
  4. Fill in a test shipping address
  5. Use test credit card: 4242 4242 4242 4242
  6. Complete the purchase

6.3 Verify Order Processing

  1. Check Forma3D.Connect Dashboard:
curl https://staging-connect-api.forma3d.be/api/v1/orders \
  -H "Authorization: Bearer $API_KEY"
  1. Check Order Status:
  2. Status should be PENDINGPROCESSING
  3. Line item should show the correct product

  4. Check Print Job:

  5. A print job should be created in SimplyPrint
  6. Verify the correct file was queued (green Benchy)

6.4 Simulate Print Completion

To simulate print completion without waiting for a real print:

  1. In SimplyPrint dashboard, find the job in the queue
  2. Manually mark the job as Completed (or Failed to test error handling)
  3. SimplyPrint will send a webhook to Forma3D.Connect with the status update

Alternatively, if webhooks are not configured, the reconciliation service will poll SimplyPrint and detect the status change.

The webhook will notify Forma3D.Connect of completion.

6.5 Verify Shipping Label Creation

After all print jobs complete:

  1. Check the order status changed to COMPLETED
  2. Verify shipment was created:
    curl https://staging-connect-api.forma3d.be/api/v1/orders/ORDER_ID \
      -H "Authorization: Bearer $API_KEY"
    
  3. Check for trackingNumber and trackingUrl in the response

  4. In Sendcloud dashboard, verify the test label was created

6.6 Verify Shopify Fulfillment

  1. Go to Shopify admin → Orders
  2. The order should show as Fulfilled
  3. Tracking information should be visible

Flow Diagram: How Color Selection Works

uml diagram


Troubleshooting

Order Not Appearing in Forma3D.Connect

  1. Verify API-registered webhooks exist (they won't show in Shopify admin UI):
# SSH into the server and check via the Shopify API
ssh root@167.172.45.47
docker exec forma3d-api node -e '...' # see Section 1.4 for full script
  1. Check API logs for incoming webhooks:
ssh root@167.172.45.47
docker logs forma3d-api --since 30m 2>&1 | grep -i webhook
  1. Verify webhook URL is correct (should be /api/v1/webhooks/shopify, not /shopify/webhooks)
  2. Common mistake: Do not rely on the Shopify Admin webhooks page — API-registered webhooks are invisible there. If you see webhooks in the admin UI, they were created manually and may point to the wrong URL.
  1. Check product mapping exists:
curl https://staging-connect-api.forma3d.be/api/v1/product-mappings \
  -H "Authorization: Bearer $API_KEY" | jq '.[] | select(.sku == "BENCHY-GREEN-001")'
  1. Check assembly part has SimplyPrint file ID:
  2. The simplyPrintFileId must match an actual file in SimplyPrint

  3. Check event logs:

    curl https://staging-connect-api.forma3d.be/api/v1/event-logs?orderId=ORDER_ID \
      -H "Authorization: Bearer $API_KEY"
    
    Look for product.unmapped events.

SimplyPrint Job Not Created

  1. Verify SimplyPrint connection:
curl https://staging-connect-api.forma3d.be/health/dependencies
  1. Check SimplyPrint API key and company ID in variable group

  2. Verify file exists in SimplyPrint:

  3. Log into SimplyPrint dashboard
  4. Check Files section for the referenced file ID

Shipping Label Not Created

  1. Verify shipping is enabled:
  2. SHIPPING_ENABLED must be true in variable group

  3. Check Sendcloud credentials:

  4. Public key and secret key must be correct

  5. Check order has valid shipping address:

  6. Country code must be valid ISO format (e.g., BE, NL, DE)

  7. Check event logs for shipment errors

If the Sendcloud shipping notification email mentions "Volg mijn pakket" / "Suivre mon colis" (Track my package) but does not contain a clickable tracking link or button, this is a Sendcloud dashboard configuration issue — not a code issue.

Forma3D.Connect correctly sends order_number, email, and request_label: true when creating parcels, and the Sendcloud API response includes tracking_url and tracking_number which are stored and synced to Shopify. The email content itself is controlled entirely by Sendcloud's platform.

To fix:

  1. Log into Sendcloud Panel
  2. Go to SettingsBrands → select your brand (or create one)
  3. Under Tracking page, ensure the tracking page is enabled
  4. This generates the tracking page URL that powers the "Track my package" button in emails
  5. Under Email notifications, verify that:
  6. Tracking email templates are enabled
  7. The tracking link/button is included in the template (preview the template to confirm)
  8. Make sure the brand is linked to your integration:
  9. Go to SettingsIntegrations → your Sendcloud API integration
  10. Ensure the correct brand is selected

Note: The tracking link typically becomes "live" (with real carrier tracking data) only after the carrier scans the package. However, the button/link itself should always appear in the email regardless of scan status.

Sendcloud brand documentation: How to set up your brand

The Sendcloud tracking email displays the Shopify order number (e.g., #1005) as plain text via the order_number field sent during parcel creation. Sendcloud does not support adding a custom link to the Shopify order in their email templates via the API.

Workaround options:

  1. Sendcloud brand email templates — In SettingsBrandsEmail notifications, check if the template editor allows adding custom text or links. You may be able to add a static link to your Shopify store's order status page.

  2. Shopify order status URL — Shopify provides an order status page to customers at checkout. This URL is already included in Shopify's own order confirmation email, so customers have it from the start.

  3. Sendcloud's order_number field — Forma3D.Connect already sends the Shopify order number (#1005) which Sendcloud displays in the email. This helps customers cross-reference with their Shopify confirmation email.

Bottom line: Adding a clickable Shopify order link in Sendcloud emails is a Sendcloud platform limitation. The order number is displayed but Sendcloud does not support hyperlinking it to an external URL via the API.

Wrong File Being Printed

  1. Verify product mapping matches:
  2. Matching uses Shopify Product ID + Variant ID first, then falls back to SKU
  3. If using SKU matching, the SKU in Shopify variant must match the ProductMapping SKU exactly (case-sensitive)
  4. Ensure the ProductMapping has the correct Shopify Product ID configured

  5. Check you have separate mappings for each color:

  6. Each color variant needs its own ProductMapping with its own file ID

If a print job is stuck in PRINTING status (the webhook from SimplyPrint was missed), you have several options:

  1. Navigate to the order detail page in the Forma3D.Connect dashboard
  2. Find the stuck print job
  3. Click the warning icon (⚠️) next to the job status
  4. Select the appropriate status:
  5. Mark Completed — if the print actually finished successfully
  6. Mark Failed — if the print failed
  7. Mark Cancelled — if the print was cancelled
  8. Enter a reason (required for audit trail)

Option 2: Use the Admin API

# Force a job to COMPLETED status
curl -X POST "https://staging-connect-api.forma3d.be/api/v1/admin/print-jobs/JOB_ID/force-status" \
  -H "Authorization: Bearer $INTERNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "COMPLETED",
    "reason": "Webhook missed - confirmed completed in SimplyPrint dashboard"
  }'

Option 3: Retry a Failed or Cancelled Job

If a job was incorrectly marked or you want to re-print:

  1. Use the Retry button in the UI (available for FAILED or CANCELLED jobs)
  2. Or use the API: POST /api/v1/print-jobs/:id/retry

This will reset the job to QUEUED and create a new job in SimplyPrint.

Stuck Job Monitoring

The system automatically monitors for stuck jobs:

  • Check interval: Every 30 minutes
  • Default threshold: 12 hours (configurable per tenant)
  • Alerts: Email notification to operators when stuck jobs are found

Configure the threshold:

# Get current threshold
curl "https://staging-connect-api.forma3d.be/api/v1/admin/print-jobs/stuck/threshold" \
  -H "Authorization: Bearer $INTERNAL_API_KEY"

# Set threshold to 24 hours (for long prints)
curl -X PATCH "https://staging-connect-api.forma3d.be/api/v1/admin/print-jobs/stuck/threshold" \
  -H "Authorization: Bearer $INTERNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"thresholdHours": 24}'

Manually check for stuck jobs:

curl "https://staging-connect-api.forma3d.be/api/v1/admin/print-jobs/stuck" \
  -H "Authorization: Bearer $INTERNAL_API_KEY"

Push Notifications Not Working

Server says "Push notifications not configured"

  1. Check VAPID environment variables are set:
ssh root@167.172.45.47
grep VAPID /opt/forma3d/.env

You should see VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT.

  1. Verify docker-compose passes VAPID vars:
grep VAPID /opt/forma3d/docker-compose.yml
  1. Recreate API container after adding VAPID:
cd /opt/forma3d && docker compose up -d --force-recreate api
  1. Test the VAPID endpoint:
    curl https://staging-connect-api.forma3d.be/api/v1/push/vapid-public-key
    
    Should return {"publicKey":"BGAS6q-..."}, not {"publicKey":null}.

Notifications sent but not received (macOS/Chrome)

  1. Check macOS System Notifications:
  2. Open System Settings → Notifications → Google Chrome
  3. Ensure notifications are allowed
  4. Check "Allow notifications" is enabled

  5. Check Focus Mode / Do Not Disturb:

  6. Look for moon icon in menu bar
  7. Disable Focus mode if enabled

  8. Check Chrome site permissions:

  9. Go to chrome://settings/content/notifications
  10. Verify staging-connect.forma3d.be is in "Allowed" list

  11. Update the Service Worker:

  12. Open the PWA or site in Chrome
  13. Press Cmd+Option+IApplication tab → Service Workers
  14. If there's a "waiting" worker, click skipWaiting
  15. Or click Update to force update

  16. Verify Service Worker has push handler: In DevTools Console, run:

    navigator.serviceWorker.ready.then((reg) => console.log('SW:', reg.active?.scriptURL));
    
    Should show the sw.js URL.

Notifications work on mobile but not desktop

  1. Check if PWA needs update:
  2. The desktop PWA may have an old service worker without push handling
  3. Uninstall and reinstall the PWA, or force update via DevTools

  4. Clear service worker and reinstall:

  5. Go to chrome://serviceworker-internals
  6. Find staging-connect.forma3d.be and click Unregister
  7. Reinstall the PWA from the browser

Stale subscriptions (sent count higher than devices receiving)

Old/invalid push subscriptions are automatically removed when they fail with a 410 Gone status. However, some stale subscriptions may linger if the push service hasn't invalidated them yet.

To manually check/clean subscriptions:

# Connect to database and list subscriptions
docker compose exec api npx prisma db execute --stdin <<< "SELECT id, \"userAgent\", \"createdAt\" FROM \"PushSubscription\" ORDER BY \"createdAt\" DESC;"

Part 7: AI-Assisted Development with MCP Servers and CLIs

This section documents how to configure MCP (Model Context Protocol) servers and CLI tools for Shopify, Sendcloud, and SimplyPrint to enable AI-assisted development, testing, and maintenance in Cursor.

7.1 Overview of MCP and CLI Integration

uml diagram

7.2 Shopify: Official MCP Server and CLI

Shopify provides official MCP servers and a comprehensive CLI tool.

7.2.1 Shopify Dev MCP Server

The Shopify Dev MCP server gives AI agents direct access to Shopify's development resources including API schemas, documentation, and Liquid validation tools.

Features:

  • Exposes Shopify's Admin GraphQL, Functions, Partner API, Storefront API schemas
  • Theme/Liquid validation (syntax checks, missing references)
  • Documentation search and retrieval
  • Polaris component information

Prerequisites:

  • Node.js 18+ (recommended: Node.js 20.10+)

Installation & Configuration:

  1. Add to Cursor MCP configuration (.cursor/mcp.json or ~/.cursor/mcp.json):
{
  "mcpServers": {
    "shopify-dev-mcp": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    }
  }
}
  1. Restart Cursor to load the MCP server

  2. Verify by asking Cursor: "What fields are available on the Order type in Shopify Admin GraphQL?"

Use Cases for Forma3D.Connect:

  • Get accurate Shopify GraphQL schemas for order/fulfillment operations
  • Validate webhook payload structures
  • Look up correct API endpoints and authentication methods
  • Generate schema-correct code for Shopify integration

Additional Shopify MCP Servers:

MCP Server Purpose Use Case
@shopify/dev-mcp Developer docs, APIs, theme validation Main development
Storefront MCP Shopping agents, cart/product discovery If building customer-facing AI
Customer Accounts MCP Order history, returns, account data If accessing customer data via AI

Resources:

  • GitHub: https://github.com/Shopify/dev-mcp
  • Docs: https://shopify.dev/apps/build/devmcp

7.2.3 Shopify Admin MCP Servers (Community)

In addition to the official Dev MCP, there are community-built Admin MCP servers that can perform actual CRUD operations on your Shopify store (products, orders, customers, inventory, etc.).

Popular Options:

Package Tools Features
@ajackus/shopify-mcp-server 70+ Products, inventory, collections, orders, fulfillment, discounts, metafields
@tzenderman/shopify-mcp Many GraphQL-based, multi-store support
shopify-mcp-server Basic Products, customers, orders

Installation & Configuration:

# Install globally
npm install -g @ajackus/shopify-mcp-server

# Or use npx
npx @ajackus/shopify-mcp-server

Add to Cursor MCP configuration:

{
  "mcpServers": {
    "shopify-admin": {
      "command": "npx",
      "args": ["-y", "@ajackus/shopify-mcp-server"],
      "env": {
        "SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx",
        "SHOPIFY_STORE_DOMAIN": "your-store.myshopify.com"
      }
    }
  }
}

Available Operations:

Category Operations
Products List, search, get by ID, create, update, delete, manage variants/images
Orders List, get details, create draft orders, fulfill, cancel, add notes
Customers Search, list, get details, create, manage tags
Inventory View levels, update inventory, track variants
Collections Manage manual & smart collections
Fulfillment Create fulfillments, add tracking, update status
Discounts Create/manage discount codes
Metafields Read/write custom fields

Required Shopify App Scopes:

For Forma3D.Connect operations, ensure your custom app has:

  • read_orders, write_orders
  • read_products, write_products
  • read_fulfillments, write_fulfillments
  • read_inventory, write_inventory
  • read_merchant_managed_fulfillment_orders, write_merchant_managed_fulfillment_orders

Use Cases for Forma3D.Connect:

  • Test order creation and fulfillment flows via AI
  • Debug webhook payloads by fetching actual order data
  • Verify product/variant configurations
  • Test fulfillment tracking updates

Security Considerations:

  • Never commit access tokens to version control
  • Use minimal required scopes
  • Consider using a separate test store for AI operations
  • Be cautious with write operations on production data

Resources:

  • @ajackus/shopify-mcp-server: https://www.npmjs.com/package/@ajackus/shopify-mcp-server
  • @tzenderman/shopify-mcp: https://github.com/tzenderman/shopify-mcp

7.2.2 Shopify CLI

The Shopify CLI is a comprehensive command-line tool for app and theme development.

Installation:

# npm
npm install -g @shopify/cli@latest

# pnpm (recommended for this project)
pnpm install -g @shopify/cli@latest

# Homebrew (macOS)
brew tap shopify/shopify
brew install shopify-cli

Key Commands:

# Authentication
shopify auth login                    # Log in to Shopify Partners
shopify auth logout                   # Log out

# App Development
shopify app init                      # Create new app
shopify app dev                       # Start development server
shopify app deploy                    # Deploy app changes

# Theme Development
shopify theme dev                     # Start theme dev server
shopify theme pull                    # Download theme from store
shopify theme push                    # Upload theme to store
shopify theme console                 # Headless Liquid REPL

# Information
shopify version                       # Check CLI version

Configuration for Forma3D.Connect:

Create a .shopify-cli.json in the project root if needed:

{
  "project_type": "custom_app",
  "organization_id": "YOUR_PARTNER_ORG_ID"
}

Use Cases:

  • Test webhook configurations locally with shopify app dev
  • Validate Shopify app configurations
  • Manage development store connections

7.3 Sendcloud: Custom MCP Server (No Official Support)

Sendcloud does not have an official MCP server or CLI tool. However, you can create a custom MCP server that wraps their REST API.

7.3.1 Sendcloud API Overview

Sendcloud provides REST APIs for:

  • Shipping API: Create parcels, generate labels, compare rates
  • Service Points API: Retrieve pickup locations
  • Returns API (V3): Handle return shipments
  • Tracking API: Track shipment status
  • Dynamic Checkout API: Service options based on zones/time

Authentication: Basic Auth with public/secret keys or OAuth2 (beta)

7.3.2 Building a Custom Sendcloud MCP Server

You can create a custom MCP server to expose Sendcloud operations to Cursor AI.

Create tools/sendcloud-mcp/server.ts:

import { MCPServer, Tool } from '@modelcontextprotocol/sdk';

const SENDCLOUD_PUBLIC = process.env.SENDCLOUD_PUBLIC_KEY;
const SENDCLOUD_SECRET = process.env.SENDCLOUD_SECRET_KEY;
const AUTH = Buffer.from(`${SENDCLOUD_PUBLIC}:${SENDCLOUD_SECRET}`).toString('base64');

const BASE_URL = 'https://panel.sendcloud.sc/api/v2';

async function fetchSendcloud(endpoint: string, options: RequestInit = {}) {
  const response = await fetch(`${BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      Authorization: `Basic ${AUTH}`,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  return response.json();
}

const server = new MCPServer();

// Tool: Get shipping methods
server.tool({
  name: 'sendcloud_get_shipping_methods',
  description: 'List available shipping methods/carriers for the account',
  parameters: {
    type: 'object',
    properties: {
      sender_country: { type: 'string', description: 'ISO country code of sender' },
      to_country: { type: 'string', description: 'ISO country code of destination' },
    },
  },
  handler: async (params) => {
    const result = await fetchSendcloud('/shipping_methods');
    return result;
  },
});

// Tool: Create parcel/label
server.tool({
  name: 'sendcloud_create_parcel',
  description: 'Create a parcel and generate shipping label',
  parameters: {
    type: 'object',
    properties: {
      name: { type: 'string', description: 'Recipient name' },
      address: { type: 'string', description: 'Street address' },
      city: { type: 'string', description: 'City' },
      postal_code: { type: 'string', description: 'Postal code' },
      country: { type: 'string', description: 'ISO country code' },
      shipment_id: { type: 'number', description: 'Shipping method ID' },
      weight: { type: 'number', description: 'Weight in grams' },
    },
    required: ['name', 'address', 'city', 'postal_code', 'country', 'shipment_id'],
  },
  handler: async (params) => {
    const result = await fetchSendcloud('/parcels', {
      method: 'POST',
      body: JSON.stringify({ parcel: params }),
    });
    return result;
  },
});

// Tool: Track parcel
server.tool({
  name: 'sendcloud_track_parcel',
  description: 'Get tracking information for a parcel',
  parameters: {
    type: 'object',
    properties: {
      parcel_id: { type: 'number', description: 'Sendcloud parcel ID' },
    },
    required: ['parcel_id'],
  },
  handler: async (params) => {
    const result = await fetchSendcloud(`/parcels/${params.parcel_id}`);
    return result;
  },
});

// Run server
server.run({ transport: 'stdio' });

Add to Cursor MCP configuration:

{
  "mcpServers": {
    "sendcloud": {
      "command": "npx",
      "args": ["ts-node", "tools/sendcloud-mcp/server.ts"],
      "env": {
        "SENDCLOUD_PUBLIC_KEY": "your-public-key",
        "SENDCLOUD_SECRET_KEY": "your-secret-key"
      }
    }
  }
}

Alternative: Use MCP SDK directly

npm install @modelcontextprotocol/sdk

7.3.3 Sendcloud API Reference

For building MCP tools, reference these key endpoints:

Endpoint Method Purpose
/shipping_methods GET List available carriers
/parcels POST Create parcel/label
/parcels/{id} GET Get parcel details/tracking
/parcels/{id}/cancel POST Cancel a parcel
/returns POST Create return label
/service-points GET Find pickup locations

API Documentation: https://www.sendcloud.dev/docs/

7.4 SimplyPrint: Community MCP Server + API

SimplyPrint does not have an official MCP server or CLI tool. However, there are community solutions and their comprehensive API can be wrapped.

7.4.1 Community 3D Printer MCP Server

The mcp-3d-printer-server by DMontgomery40 supports multiple 3D printer backends including connections compatible with SimplyPrint workflows.

Features:

  • Printer status queries (temps, progress)
  • File management (upload, list, delete)
  • Print job control (start, cancel)
  • STL manipulation (scale, rotate, slice)
  • Supports: OctoPrint, Klipper/Moonraker, Duet, Repetier, Bambu Labs, Prusa Connect, Creality

Installation:

# Global install
npm install -g mcp-3d-printer-server

# Or from source
git clone https://github.com/dmontgomery40/mcp-3d-printer-server.git
cd mcp-3d-printer-server
npm install
npm link

Configuration (.env):

# Printer connection
PRINTER_HOST=192.168.1.100
PRINTER_PORT=80
PRINTER_TYPE=octoprint    # octoprint, klipper, bambu, prusa, creality
API_KEY=your_printer_api_key

# Temporary file storage
TEMP_DIR=/tmp/mcp-3d-temp

# For Bambu Lab printers
BAMBU_SERIAL=YOUR_PRINTER_SERIAL
BAMBU_TOKEN=YOUR_ACCESS_TOKEN

# Slicer settings (optional)
SLICER_TYPE=prusaslicer
SLICER_PATH=/usr/local/bin/prusaslicer
SLICER_PROFILE=/path/to/profile.ini

Add to Cursor MCP configuration:

{
  "mcpServers": {
    "3d-printer": {
      "command": "mcp-3d-printer-server",
      "env": {
        "PRINTER_HOST": "192.168.1.100",
        "PRINTER_TYPE": "octoprint",
        "API_KEY": "your-api-key"
      }
    }
  }
}

Available Tools:

Tool Description
get_status Printer state, temperatures, job progress
list_files List G-code/design files on printer
upload_file Upload G-code to printer
start_print Start a print job
cancel_print Cancel current print
set_temperature Set nozzle/bed temperature
transform_model Scale, rotate, translate STL
slice_model Convert STL to G-code

Resources:

  • GitHub: https://github.com/DMontgomery40/mcp-3d-printer-server
  • MCP Hub: https://mcphub.tools/detail/DMontgomery40/mcp-3D-printer-server

7.4.2 SimplyPrint API Overview

SimplyPrint provides a comprehensive REST API for print farm management.

Base URL: https://api.simplyprint.io/{company_id}/

Authentication:

  • Header: X-API-KEY: {API_KEY}
  • Requires Pro or Team plan

Key Endpoints:

Category Endpoint Purpose
Printers GET /printers List printers with status
Files POST /files/Upload Upload G-code/STL
Queue POST /queue/AddItem Add to print queue
Jobs GET /jobs List print jobs
Filament GET /filaments Manage filament profiles
Webhooks POST /webhooks Subscribe to events

Webhook Events:

  • job.started, job.done, job.failed, job.cancelled
  • printer.material_changed, printer.status_changed

7.4.3 Building a Custom SimplyPrint MCP Server

Create a custom MCP server for SimplyPrint operations:

Create tools/simplyprint-mcp/server.ts:

import { MCPServer } from '@modelcontextprotocol/sdk';

const SIMPLYPRINT_API_KEY = process.env.SIMPLYPRINT_API_KEY;
const SIMPLYPRINT_COMPANY_ID = process.env.SIMPLYPRINT_COMPANY_ID;
const BASE_URL = `https://api.simplyprint.io/${SIMPLYPRINT_COMPANY_ID}`;

async function fetchSimplyPrint(endpoint: string, options: RequestInit = {}) {
  const response = await fetch(`${BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      'X-API-KEY': SIMPLYPRINT_API_KEY!,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  return response.json();
}

const server = new MCPServer();

// Tool: Get printers
server.tool({
  name: 'simplyprint_get_printers',
  description: 'List all printers and their current status',
  parameters: { type: 'object', properties: {} },
  handler: async () => {
    return await fetchSimplyPrint('/printers');
  },
});

// Tool: Get print queue
server.tool({
  name: 'simplyprint_get_queue',
  description: 'Get the current print queue',
  parameters: { type: 'object', properties: {} },
  handler: async () => {
    return await fetchSimplyPrint('/queue');
  },
});

// Tool: Add to queue
server.tool({
  name: 'simplyprint_add_to_queue',
  description: 'Add a file to the print queue',
  parameters: {
    type: 'object',
    properties: {
      file_id: { type: 'string', description: 'SimplyPrint file ID' },
      printer_id: { type: 'string', description: 'Target printer ID (optional)' },
      copies: { type: 'number', description: 'Number of copies', default: 1 },
    },
    required: ['file_id'],
  },
  handler: async (params) => {
    return await fetchSimplyPrint('/queue/AddItem', {
      method: 'POST',
      body: JSON.stringify(params),
    });
  },
});

// Tool: Get job status
server.tool({
  name: 'simplyprint_get_job',
  description: 'Get status of a specific print job',
  parameters: {
    type: 'object',
    properties: {
      job_id: { type: 'string', description: 'Job ID' },
    },
    required: ['job_id'],
  },
  handler: async (params) => {
    return await fetchSimplyPrint(`/jobs/${params.job_id}`);
  },
});

server.run({ transport: 'stdio' });

Add to Cursor MCP configuration:

{
  "mcpServers": {
    "simplyprint": {
      "command": "npx",
      "args": ["ts-node", "tools/simplyprint-mcp/server.ts"],
      "env": {
        "SIMPLYPRINT_API_KEY": "your-api-key",
        "SIMPLYPRINT_COMPANY_ID": "your-company-id"
      }
    }
  }
}

API Documentation: https://apidocs.simplyprint.io/

7.5 Complete Cursor MCP Configuration

Here's a complete .cursor/mcp.json configuration for all integrations:

{
  "mcpServers": {
    "shopify-dev-mcp": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    },
    "shopify-admin": {
      "command": "npx",
      "args": ["-y", "@ajackus/shopify-mcp-server"],
      "env": {
        "SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx",
        "SHOPIFY_STORE_DOMAIN": "your-store.myshopify.com"
      }
    },
    "3d-printer": {
      "command": "mcp-3d-printer-server",
      "env": {
        "PRINTER_HOST": "192.168.1.100",
        "PRINTER_TYPE": "bambu",
        "BAMBU_SERIAL": "YOUR_SERIAL",
        "BAMBU_TOKEN": "YOUR_TOKEN"
      }
    },
    "simplyprint": {
      "command": "npx",
      "args": ["ts-node", "tools/simplyprint-mcp/server.ts"],
      "env": {
        "SIMPLYPRINT_API_KEY": "${SIMPLYPRINT_API_KEY}",
        "SIMPLYPRINT_COMPANY_ID": "${SIMPLYPRINT_COMPANY_ID}"
      }
    },
    "sendcloud": {
      "command": "npx",
      "args": ["ts-node", "tools/sendcloud-mcp/server.ts"],
      "env": {
        "SENDCLOUD_PUBLIC_KEY": "${SENDCLOUD_PUBLIC_KEY}",
        "SENDCLOUD_SECRET_KEY": "${SENDCLOUD_SECRET_KEY}"
      }
    }
  }
}

Note: Replace ${VAR} placeholders with actual values or use environment variable references.

7.6 Summary: Integration Status

Service Official MCP Community MCP Official CLI API Available
Shopify Yes (@shopify/dev-mcp) Yes (@ajackus/shopify-mcp-server) Yes (@shopify/cli) Yes
Sendcloud No No (build yourself) No Yes
SimplyPrint No Partial (mcp-3d-printer-server) No Yes

For optimal AI-assisted development:

  1. Always install Shopify Dev MCP - Provides schema-accurate code generation for orders, fulfillments, webhooks

  2. Consider Shopify Admin MCP (@ajackus/shopify-mcp-server) - Allows AI to perform actual operations on your test store (fetch orders, create fulfillments, etc.)

  3. Install Shopify CLI globally - Useful for testing webhooks and app configurations

  4. Consider building custom MCP servers for Sendcloud and SimplyPrint if you frequently need AI assistance with:

  5. Debugging shipping label creation
  6. Testing print queue operations
  7. Understanding API response structures

  8. For 3D printer debugging, the community mcp-3d-printer-server can connect directly to your printers for status queries during development

Minimum recommended configuration:

{
  "mcpServers": {
    "shopify-dev-mcp": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    }
  }
}

Full recommended configuration for development/testing:

{
  "mcpServers": {
    "shopify-dev-mcp": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    },
    "shopify-admin": {
      "command": "npx",
      "args": ["-y", "@ajackus/shopify-mcp-server"],
      "env": {
        "SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx",
        "SHOPIFY_STORE_DOMAIN": "forma3d-test.myshopify.com"
      }
    }
  }
}

This gives Cursor AI:

  • Dev MCP: Accurate Shopify API schemas and documentation
  • Admin MCP: Ability to fetch/manipulate actual store data for debugging and testing

Part 8: Shopify Theme Deployment

The Forma3D Shopify theme is located in deployment/shopify-theme/ and implements the design system from docs/09-design/.

8.1 Theme Overview

The theme is a Shopify 2.0 theme with:

  • Studio Neat-inspired minimal aesthetic — Clean typography, generous whitespace
  • Sage green color palette — Primary #69A88E, coordinated secondary colors
  • IKEA compatibility badges — Product cards show "Fits IKEA FÄRGKLAR" badges
  • Grid configurator page — Custom page template for drawer size configuration
  • Cart drawer — Slide-out AJAX cart for seamless shopping
  • Mobile-first responsive design — Works on all devices

8.2 Theme Installation

# Navigate to the theme directory
cd deployment/shopify-theme

# Log in to Shopify Partners (if not already logged in)
shopify auth login

# Push theme to your development store
shopify theme push --store forma3d-dev.myshopify.com

# Or start a development server with live preview
shopify theme dev --store forma3d-dev.myshopify.com

Option B: Manual ZIP Upload

  1. Create ZIP archive:
cd deployment
zip -r forma3d-theme.zip shopify-theme/
  1. Upload in Shopify Admin:
  2. Go to https://forma3d-dev.myshopify.com/admin
  3. Navigate to Online StoreThemes
  4. Click Add themeUpload ZIP file
  5. Select forma3d-theme.zip
  6. Click Upload file

  7. Publish the theme:

  8. After upload, click ActionsPublish on the new theme

8.3 Theme Configuration

After installation, customize the theme in Shopify:

  1. Go to Online StoreThemes
  2. Click Customize on the Forma3D theme

Header Settings

Setting Recommended Value
Logo Upload forma3d-icon.svg or leave blank for text logo
Logo Width 120px
Use Forma3D Icon Logo Enabled
Navigation Menu Create a menu with: Shop, About, Contact
Announcement Bar "Not sure what fits? Check our IKEA compatibility guide →"

Color Settings

The theme uses the Forma3D design system colors by default:

Color Default
Primary (Sage Green) #69A88E
Secondary #C5D6C9
Tertiary #E8F0E9
Text Dark #212121
Background #FFFFFF

Product Settings

Setting Purpose
Show IKEA Compatibility Badge Displays "Fits IKEA ..." badge on product cards
Show Quick Add Button Enables one-click add to cart on collection pages

8.4 Setting Up the Grid Configurator Page

  1. Create a new page:
  2. Go to Online StorePages
  3. Click Add page
  4. Title: "Grid Configurator"
  5. Template: Select page.configurator
  6. Save

  7. Add to navigation:

  8. Go to Online StoreNavigation
  9. Edit your main menu
  10. Add "Customize Your Grid" linking to the configurator page

8.5 Product Setup for IKEA Compatibility Badges

Products can display IKEA compatibility badges in two ways:

Method 1: Product Tags (Simple)

Add tags to your products in this format:

  • IKEA-FÄRGKLAR → Shows "Fits IKEA FÄRGKLAR"
  • IKEA-MAXIMERA → Shows "Fits IKEA MAXIMERA"
  • IKEA-365+ → Shows "Fits IKEA 365+"

Method 2: Metafields (Advanced)

  1. Go to SettingsCustom dataProducts
  2. Add a metafield definition:
  3. Name: IKEA Compatible
  4. Namespace and key: custom.ikea_compatible
  5. Type: Single line text
  6. On each product, set the value (e.g., "FÄRGKLAR", "MAXIMERA")

8.6 Theme File Structure

deployment/shopify-theme/
├── assets/
│   ├── base.css              # Design tokens, reset, utilities
│   ├── components.css        # Buttons, forms, cards, badges
│   ├── sections.css          # Header, footer, hero, product page
│   ├── global.js             # Cart drawer, mobile nav, interactions
│   ├── forma3d-icon.svg      # Symbolic 3D cube logo
│   └── forma3d-organize-logo.svg  # Full product wordmark
├── config/
│   ├── settings_schema.json  # Theme customizer settings
│   └── settings_data.json    # Default values
├── layout/
│   └── theme.liquid          # Main layout wrapper
├── locales/
│   └── en.default.json       # English translations
├── sections/
│   ├── header.liquid         # Site header with nav
│   ├── footer.liquid         # Site footer
│   ├── hero.liquid           # Homepage hero section
│   ├── trust-bar.liquid      # Trust badges (EU, shipping, returns)
│   ├── featured-products.liquid
│   ├── grid-cta.liquid       # Configurator promotion
│   ├── main-product.liquid   # Product detail page
│   ├── main-collection.liquid # Collection page with filters
│   ├── configurator.liquid   # Grid configurator
│   └── ...
├── snippets/
│   ├── forma3d-icon.liquid   # Symbolic logo component
│   ├── forma3d-logo.liquid   # Full logo component
│   ├── product-card.liquid   # Product card with badges
│   ├── cart-drawer.liquid    # Slide-out cart
│   └── icon-*.liquid         # UI icons
├── templates/
│   ├── index.json            # Homepage
│   ├── product.json          # Product pages
│   ├── collection.json       # Collection pages
│   ├── page.configurator.json # Grid configurator
│   └── ...
└── README.md                 # Theme documentation

8.7 Troubleshooting Theme Issues

Theme Not Uploading

  1. Check file size: Max 50MB for ZIP upload
  2. Validate JSON: Run cat templates/*.json | jq . to check syntax
  3. Check file structure: Ensure all required folders exist (layout, templates, config)

Styles Not Loading

  1. Clear browser cache: Hard refresh with Cmd+Shift+R
  2. Check asset paths: Verify CSS files exist in assets/ folder
  3. Check theme.liquid: Ensure stylesheet tags are present

Logo Not Showing

  1. Check settings: Theme Customize → Header → Logo settings
  2. Use icon logo: Enable "Use Forma3D Icon Logo" if no custom logo uploaded
  3. Check snippet: The forma3d-icon.liquid snippet must exist

IKEA Badges Not Displaying

  1. Check product tags: Must start with IKEA- (e.g., IKEA-FÄRGKLAR)
  2. Check settings: Enable "Show IKEA Compatibility Badge" in Theme Settings
  3. Check metafields: If using metafields, verify namespace is custom.ikea_compatible

8.8 Theme Development Workflow

For ongoing theme development:

# Start live development server
cd deployment/shopify-theme
shopify theme dev --store forma3d-dev.myshopify.com

# This opens a preview URL (e.g., https://127.0.0.1:9292)
# Changes to files are reflected immediately

# Push changes to store
shopify theme push

# Pull changes made in Shopify admin
shopify theme pull

8.9 Service Point Delivery Setup & Testing

The theme includes a Sendcloud service point picker that lets customers choose between home delivery and pickup at a nearby location. This requires additional configuration beyond installing the theme.

Prerequisites

  • Sendcloud account with API credentials (see Part 3)
  • Theme installed and published (see 8.2)

Step 1: Configure Sendcloud API Key in Theme Settings

  1. Go to Online StoreThemesCustomize
  2. Click Theme settings (gear icon in the bottom-left)
  3. Find the Sendcloud section
  4. Enter your Sendcloud API key in the sendcloud_api_key field
  5. Optionally configure allowed carriers (default: ["postnl", "dhl", "dpd"])
  6. Save

Step 2: Add Service Point Snippets to Checkout

The theme includes these files that power the service point flow:

File Purpose
assets/service-point-picker.js Integrates the Sendcloud picker widget
snippets/delivery-options.liquid Renders home vs pickup selector in checkout
snippets/service-point-display.liquid Shows selected service point on order confirmation
snippets/service-point-email.liquid Shows service point address in email notifications

If using a custom checkout layout, ensure these are included:

{{ 'service-point-picker.js' | asset_url | script_tag }}
{% render 'delivery-options' %}

For order confirmation pages:

{% render 'service-point-display', order: order %}

For email templates (Settings → Notifications → Order confirmation), copy the content from service-point-email.liquid after the shipping address section.

Step 3: Verify the Flow

  1. Place a test order and select "Pickup Point" during checkout
  2. The Sendcloud widget should open a map with available service points
  3. After selecting a point, verify the address appears in the checkout
  4. Complete the order
  5. In Shopify Admin → Orders, open the order and check Additional details — you should see note_attributes like:
  6. delivery_type: service_point
  7. service_point_id: (numeric ID)
  8. service_point_name: (e.g., "PostNL Punt - Albert Heijn")
  9. service_point_street, service_point_city, service_point_postal_code, service_point_country, service_point_carrier
  10. Forma3D.Connect reads these note_attributes from the webhook and includes to_service_point when creating the Sendcloud parcel

Troubleshooting

Issue Solution
Picker doesn't open Check that sendcloud_api_key is set in theme settings; check browser console for script errors
No service points shown Verify the customer's postal code and country are supported by your Sendcloud carriers
Service point data missing in order Confirm delivery-options.liquid is rendered in the checkout form so hidden fields are submitted

Part 9: Grid Product Configuration in Shopify

This section explains how to create and configure a grid product in Shopify so that customers can order custom-size Gridfinity baseplates through the theme's grid configurator.

9.1 How Grid Products Work — Overview

The grid configurator allows customers to enter drawer width and depth dimensions. The system then:

  1. Theme configurator — calculates grid units and price, submits the order with dimension data as Shopify line item properties
  2. Forma3D.Connect — detects the product by its GRID-CUSTOM SKU prefix, extracts dimensions, and triggers the GridFlock pipeline
  3. GridFlock pipeline — generates STL plates, slices them to gcode, uploads to SimplyPrint, and creates a product mapping
  4. Print jobs — are created automatically once the mapping is ready

9.2 Create the Grid Product in Shopify

  1. Go to ProductsAdd product
  2. Configure the product:
Field Value
Title "Custom Grid Baseplate" (or your preferred name)
Description Describe the custom grid — mention configurable dimensions
Product type Grid Baseplate
Vendor Forma3D
Tags grid, custom, gridfinity
  1. Variants & SKU — This is critical:
  2. Create a single default variant
  3. Set the SKU to start with GRID-CUSTOM (e.g., GRID-CUSTOM-BASE)
  4. Set the price to your base price (the configurator calculates the final price dynamically)

  5. Save the product

Why GRID-CUSTOM? The orchestration service uses this SKU prefix to detect grid products. Any line item with a SKU starting with GRID-CUSTOM is routed through the GridFlock pipeline instead of the standard product-mapping flow.

9.3 Required Line Item Properties

When a customer configures and adds a grid product to cart, the configurator must submit these line item properties via the Shopify Cart API. The orchestration service extracts these to determine the STL parameters:

Property Name Type Required Example Description
Width (mm) string Yes "450" Drawer width in millimeters
Height (mm) string Yes "320" Drawer depth in millimeters
Connector Type string No "Intersection Puzzle" Connector style; defaults to intersection-puzzle
Magnets string No "true" Whether to include magnets; defaults to false

Accepted values for Connector Type:

  • Intersection Puzzle or intersection-puzzle — GRIPS-like connectors at grid cell intersections
  • Edge Puzzle or edge-puzzle — connectors along plate edges with tunable tolerance

The configurator submits these via the Shopify AJAX Cart API:

fetch('/cart/add.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: GRID_PRODUCT_VARIANT_ID,
    quantity: 1,
    properties: {
      'Width (mm)': '450',
      'Height (mm)': '320',
      'Connector Type': 'Intersection Puzzle',
      'Magnets': 'true'
    }
  })
});

9.4 Setting Up the Configurator Page

  1. Create the page (see also 8.4):
  2. Go to Online StorePagesAdd page
  3. Title: "Grid Configurator"
  4. Template: page.configurator
  5. Save

  6. Link the product variant:

  7. The configurator template (sections/configurator.liquid) must know the variant ID of your grid product to submit to the cart
  8. Update the GRID_PRODUCT_VARIANT_ID in the configurator JavaScript or configure it via theme settings

  9. Add to navigation:

  10. Go to Online StoreNavigation
  11. Edit the main menu
  12. Add "Customize Your Grid" linking to the configurator page

9.5 Configurator Pricing

The theme configurator calculates price client-side using these constants (in configurator.liquid):

Constant Default Description
UNIT_SIZE 7.5 cm Centimeters per grid unit
BASE_PRICE $20 Fixed base price
UNIT_PRICE $2 Additional price per grid unit

Formula: price = BASE_PRICE + (unitsX × unitsY × UNIT_PRICE)

Adjust these values in the <script> block of configurator.liquid to match your actual Shopify pricing.

Note: The Shopify product's price serves as a base price for the cart. If you need exact server-side pricing, consider using Shopify Scripts or a draft order flow.

9.6 Verification Checklist

After setup, verify the full flow:

  • Grid product exists with a GRID-CUSTOM SKU prefix
  • Configurator page is created with the page.configurator template
  • Configurator page is linked in the navigation menu
  • Width and depth inputs work and update the grid unit calculation
  • "Add to Cart" submits the correct line item properties (Width (mm), Height (mm), Connector Type, Magnets)
  • In the Shopify cart, the line item shows the custom properties
  • After placing a test order, Forma3D.Connect logs show "GridFlock product detected"

Part 10: GridFlock Pipeline — How Custom Grid Orders Are Processed

This section documents the end-to-end flow when a customer orders a custom-size grid product — from Shopify order to printed plates.

10.1 Pipeline Overview

┌──────────────┐     ┌──────────────────┐     ┌──────────────────┐     ┌──────────────┐
│   Shopify    │────▶│  Order Service   │────▶│ GridFlock Service│────▶│  SimplyPrint  │
│  (Webhook)   │     │ (Orchestration)  │     │   (Pipeline)     │     │ (Print Queue) │
└──────────────┘     └──────────────────┘     └──────────────────┘     └──────────────┘
                            │                         │
                            │                         │
                     1. Detect GRID-CUSTOM     4. Generate STL (JSCAD)
                     2. Extract dimensions     5. Slice → gcode
                     3. Trigger pipeline       6. Upload to SimplyPrint
                                               7. Emit mapping-ready event
                            │                         │
                            ◀─────────────────────────┘
                     8. Create print jobs
                     9. Normal fulfillment flow

10.2 Step-by-Step Flow

Step 1: SKU Detection

When a Shopify order webhook arrives, the orchestration service inspects each line item's SKU. If the SKU starts with GRID-CUSTOM, the item is routed through the GridFlock pipeline instead of the standard product-mapping lookup.

SKU: GRID-CUSTOM-BASE  →  GridFlock pipeline
SKU: BENCHY-GREEN      →  Standard product mapping

Step 2: Dimension Extraction

The orchestration service extracts grid parameters from the Shopify line item properties:

Property Extracted As
Width (mm) Target width in millimeters
Height (mm) Target depth in millimeters
Connector Type intersection-puzzle or edge-puzzle (default: intersection-puzzle)
Magnets true or false (default: false)

Step 3: Deterministic SKU Computation

A deterministic SKU is computed from the grid parameters so that identical configurations reuse the same STL/gcode files:

Format: GF-{width}x{height}-{connector}-{magnets}

Parameters Computed SKU
450mm × 320mm, intersection puzzle, magnets GF-450x320-IP-MAG
450mm × 320mm, edge puzzle, no magnets GF-450x320-EP-NOMAG
600mm × 400mm, intersection puzzle, magnets GF-600x400-IP-MAG

If a product mapping already exists for this computed SKU, the pipeline is skipped and print jobs are created immediately.

Step 4: GridFlock Pipeline (STL → gcode → SimplyPrint)

When no mapping exists, the orchestration service calls the GridFlock Service, which runs a buffer-based pipeline (no files written to disk):

4a. Calculate plate set — Based on the target dimensions and printer bed size, determine how many plates are needed and their grid sizes.

4b. Generate STL — For each plate, generate the Gridfinity baseplate geometry using JSCAD (GridFlock core library). Includes connectors, magnets, and plate numbering.

4c. Slice to gcode — Send the STL buffer to the slicer container, which produces gcode/3MF using the tenant's configured printer profile (machine, process, and filament settings).

4d. Upload to SimplyPrint — Upload the sliced gcode to SimplyPrint via the Print Service, making it available in the print queue.

Each plate is processed sequentially to bound memory usage.

Step 5: Mapping Ready Event

After all plates are uploaded, the GridFlock Service emits a gridflock.mapping-ready event containing the order ID, line item ID, and computed SKU.

Step 6: Print Job Creation

The orchestration service handles the gridflock.mapping-ready event:

  1. Looks up the line item from the original order
  2. Creates print jobs for each plate in the set
  3. The order proceeds through the normal fulfillment flow (print → ship → fulfill)

10.3 Error Handling

If any step in the pipeline fails, a gridflock.pipeline-failed event is emitted with:

Field Description
failedStep One of: stl-generation, slicing, simplyprint-upload, mapping-creation
errorMessage Human-readable error description
sku The computed GridFlock SKU
orderId The Shopify order ID
lineItemId The specific line item that failed

The operator is notified via the notifications service, and the error is logged to Sentry and the event log.

10.4 Tenant Print Settings

The GridFlock Service loads printer configuration from the SystemConfig table:

Setting Default Description
printerModel bambu-a1 Printer profile for bed size calculation
bedSize [256, 256] Printer bed dimensions in mm
nozzleDiameter 0.4 Nozzle size
layerHeight 0.2 Layer height for slicing
filamentType PLA Filament material
machineProfilePath (configured) Path to machine profile for slicer
processProfilePath (configured) Path to process profile for slicer
filamentProfilePath (configured) Path to filament profile for slicer

These can be configured per tenant in the database via the SystemConfig model.

10.4b Dimension & Pricing Settings

The following SystemConfig keys control grid dimension limits and pricing. They are read by the Order Service (grid-pricing endpoint) and the GridFlock Service (preview and pipeline).

Key Default Used By Description
gridflock.max_dimension_mm 1000 Both services Maximum allowed width or height in mm (applies to both axes independently). Dimensions exceeding this limit will: reject the preview (HTTP 204), hide the price on the storefront, disable checkout buttons, and reject pipeline generation.
grid.basePriceEur 20 Order Service (pricing) Fixed base price in EUR added to every grid order
grid.unitPriceEur 2 Order Service (pricing) Price per Gridfinity grid unit (42mm × 42mm)
grid.unitSizeMm 42 Order Service (pricing) Size of one Gridfinity grid unit in mm
grid.currency EUR Order Service (pricing) Currency code for price formatting

Changing the max dimension at runtime:

The max dimension can be updated in the database without redeployment:

-- Set max dimension to 800mm (80cm) for the default tenant
INSERT INTO "SystemConfig" ("id", "tenantId", "key", "value", "description", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), 'default', 'gridflock.max_dimension_mm', '{"value": 800}', 'Max grid dimension in mm', NOW(), NOW())
ON CONFLICT ("tenantId", "key") DO UPDATE SET "value" = '{"value": 800}', "updatedAt" = NOW();

The Shopify configurator dynamically reads the limit from the grid-pricing response (maxDimensionCm field) and displays it in the error message when exceeded.

10.5 Testing a Grid Order End-to-End

  1. Create a grid product in Shopify with SKU GRID-CUSTOM-BASE (see Part 9)
  2. Place a test order using the grid configurator with specific dimensions (e.g., 450mm × 320mm)
  3. Check Forma3D.Connect logs for:
  4. GridFlock product detected: lineItem=..., computed SKU=GF-450x320-IP-MAG
  5. GridFlock pipeline triggered for GF-450x320-IP-MAG
  6. Monitor the pipeline in the event log:
  7. gridflock.pipeline_triggered — pipeline started
  8. gridflock.mapping-ready or gridflock.pipeline_failed — pipeline result
  9. gridflock.print_jobs_created — print jobs created from the mapping
  10. Verify in SimplyPrint that the gcode files appear in the print queue
  11. For subsequent orders with the same dimensions, the pipeline should be skipped (mapping reused)

10.6 Troubleshooting GridFlock Orders

Symptom Likely Cause Resolution
Order not detected as GridFlock SKU doesn't start with GRID-CUSTOM Update the product SKU in Shopify
"Grid dimensions missing" error Line item properties missing Width (mm) or Height (mm) Verify the configurator submits properties correctly
Pipeline fails at stl-generation Dimensions exceed gridflock.max_dimension_mm or JSCAD error Check dimensions against configured max (default 1000mm / 100cm); see 10.4b
Pipeline fails at slicing Slicer container unavailable or invalid profiles Check slicer container health; verify profile paths in SystemConfig
Pipeline fails at simplyprint-upload SimplyPrint API credentials invalid or service down Check SimplyPrint API key and service status
Print jobs not created after mapping ready Event handler failed or order not found Check event log for gridflock.mapping-ready; verify order exists
Same dimensions generating new STLs SKU computation mismatch Verify connector type and magnets match between orders

10.7 Architecture Reference

For detailed technical background on the GridFlock system, see:


Part 11: Shopify App Proxy & Storefront Security

The Grid Configurator's "Buy Now" button calls a backend endpoint to create a Shopify Draft Order with custom pricing. This endpoint is public (no login required), so it needs protection against abuse. The Shopify App Proxy is the recommended approach — Shopify signs every request with an HMAC, and our backend verifies it.

11.1 How It Works

Customer's browser                 Shopify CDN                   Forma3D Backend
       │                               │                              │
       │  GET /apps/forma3d/grid-checkout                              │
       │  ──────────────────────────►   │                              │
       │                               │  POST /api/v1/storefront/    │
       │                               │  grid-checkout?signature=... │
       │                               │  &shop=...&timestamp=...     │
       │                               │  ────────────────────────►   │
       │                               │                              │ Verify HMAC
       │                               │             200 OK           │
       │                               │  ◄────────────────────────   │
       │            200 OK             │                              │
       │  ◄──────────────────────────  │                              │
  1. The storefront JavaScript calls a relative URL on the Shopify domain (/apps/forma3d/grid-checkout)
  2. Shopify intercepts the request, appends HMAC query parameters (signature, shop, timestamp, path_prefix), and forwards it to the Proxy URL configured in the app
  3. The backend's ShopifyAppProxyGuard verifies the HMAC using the app's API secret
  4. If valid, the request proceeds to StorefrontController; if invalid, a 401 Unauthorized is returned

Benefits over direct API calls: - No CORS needed (same Shopify domain) - Cryptographic proof that the request came from your Shopify store - No backend URL exposed in the storefront JavaScript

11.2 Configure the App Proxy in Shopify

  1. Go to Shopify Partners Dashboard → your app → App setupApp proxy
  2. Set:
  3. Sub path prefix: apps
  4. Sub path: forma3d
  5. Proxy URL: https://staging-connect-api.forma3d.be/api/v1/storefront
  6. Click Save

After saving, requests to https://your-store.myshopify.com/apps/forma3d/* are forwarded to https://staging-connect-api.forma3d.be/api/v1/storefront/* with Shopify's HMAC signature.

11.3 Backend Environment Variable

The HMAC verification uses the app's client secret. Set this in your deployment environment:

# Option A: Explicit (recommended for clarity)
SHOPIFY_APP_PROXY_SECRET=your-shopify-api-secret-here

# Option B: Falls back to SHOPIFY_API_SECRET automatically
# If SHOPIFY_APP_PROXY_SECRET is not set, the guard uses SHOPIFY_API_SECRET

When neither variable is set (local development), the guard passes through all requests — this allows testing without a Shopify proxy.

For Azure DevOps Pipeline Variables, add:

Variable Value Secret?
SHOPIFY_APP_PROXY_SECRET (same as SHOPIFY_API_SECRET) Yes

11.4 Theme Configuration

The grid configurator section has two URL settings:

Setting Purpose Default
App Proxy Path (recommended) Relative proxy path (e.g., /apps/forma3d). HMAC-signed by Shopify. /apps/forma3d
API Base URL (fallback) Direct backend URL (e.g., https://api.forma3d.be). Requires CORS. (empty)

The configurator tries the proxy path first. If empty, it falls back to the direct API URL.

To configure in the Shopify theme customizer:

  1. Go to Online StoreCustomize → navigate to the Grid Configurator section
  2. Set App Proxy Path to /apps/forma3d (this is the default)
  3. Leave API Base URL empty (it's only needed for local development or when the App Proxy is not set up)

11.5 Rate Limiting

In addition to HMAC verification, the storefront endpoint has a strict rate limit: 10 requests per minute per IP. This is enforced by the @StorefrontThrottle() decorator. When exceeded, the API returns 429 Too Many Requests.

11.6 Security Layers Summary

Layer Protection When Active
Shopify App Proxy HMAC Cryptographic proof request came from your Shopify store Production (secret set)
Rate limiting 10 req/min per IP — prevents draft order spam Always
Input validation Width 100–1000mm, height 100–1000mm, typed fields Always
CORS Only allows *.myshopify.com origins Direct API calls only

11.7 Local Development Without App Proxy

During local development you typically don't have a Shopify App Proxy configured. The system handles this gracefully:

  1. Leave SHOPIFY_APP_PROXY_SECRET and SHOPIFY_API_SECRET unset → the guard passes through
  2. In the theme customizer, clear the App Proxy Path field
  3. Set API Base URL to your local backend (e.g., http://localhost:3001)
  4. The configurator will use the direct API URL with CORS

11.8 Troubleshooting

Symptom Likely Cause Resolution
401 Missing proxy signature Request sent directly to backend, not through App Proxy Use the proxy path (/apps/forma3d/...) instead of direct API URL
401 Invalid proxy signature Wrong secret configured Verify SHOPIFY_APP_PROXY_SECRET matches the app's Client Secret
429 Too Many Requests Rate limit exceeded Wait 60 seconds; limit is 10 req/min per IP
Configurator says "Checkout not configured" Neither proxy path nor API URL set Set the App Proxy Path in the section settings
CORS errors in browser console Using direct API URL without proper CORS config Switch to App Proxy (no CORS needed) or verify backend CORS origins

Part 12: STL Preview Cache

The storefront configurator shows a 3D preview of the grid baseplate. The plate-level cache stores 200 base plate STLs that are assembled on the fly with dynamically generated borders.

The plate-level cache stores only 200 base plate STLs (~41 MB total). At preview request time, the service assembles a complete preview by looking up cached base plates, generating border geometry on the fly (simple rectangular STL, no JSCAD), and combining everything. This supports any input resolution (including 1 mm precision) with the same 200-file cache.

┌───────────────────────────┐         rsync over SSH          ┌──────────────────────────┐
│  Any machine              │ ──────────────────────────────> │  Staging server          │
│  (2+ cores sufficient)    │                                 │  (Docker container)      │
│                           │    plate-1x1-0000.stl           │                          │
│  $ pnpm tsx               │    plate-1x1-0100.stl           │  /data/gridflock/        │
│      --tsconfig           │    plate-2x3-1010.stl           │    plate-cache/          │
│        tsconfig.base.json │    ...                          │                          │
│      scripts/populate-    │    plate-6x6-1111.stl           │                          │
│        plate-cache.ts     │    (200 files, ~41 MB)          │                          │
│      --out ./plate-output │                                 │                          │
└───────────────────────────┘                                 └──────────────────────────┘

How assembly works at request time:

  1. calculatePlateSet() determines the plates needed (pure math, < 1 ms)
  2. Each plate's base STL is looked up from the in-memory cache by key (e.g., plate-5x5-1100.stl)
  3. Border strips are generated on the fly as simple rectangular cuboids (12 triangles each, microseconds)
  4. Everything is combined with combineStlBuffers() (buffer concatenation, < 10 ms)
  5. Total assembly time: 10–100 ms (vs 12–30 seconds for cold JSCAD generation)

Preview resolution cascade: The service tries two paths in order:

  1. Plate-level assembly — assemble from cached base plates + dynamic borders (10–100 ms)
  2. Full JSCAD generation — fallback when the plate cache is not populated (12–30 seconds)

12.2 Plate Cache: Prerequisites

# Install tsx (one-time)
pnpm add -D tsx

# Build gridflock-core (required for JSCAD plate generation)
pnpm nx build gridflock-core

12.3 Plate Cache: Step 1 — Generate Locally

The script generates all 200 base plates in 2–5 minutes on any machine:

pnpm tsx --tsconfig tsconfig.base.json scripts/populate-plate-cache.ts \
  --out ./plate-cache-output

CLI arguments:

Argument Default Description
--out ./plate-cache-output Output directory
--dry-run false Show stats without generating files
--verify false Check existing files are valid binary STL

Dry run first:

pnpm tsx --tsconfig tsconfig.base.json scripts/populate-plate-cache.ts --dry-run

Verify existing cache:

pnpm tsx --tsconfig tsconfig.base.json scripts/populate-plate-cache.ts --verify --out ./plate-cache-output

The script is resumable — re-run the same command and it skips already-generated files.

12.4 Plate Cache: Step 2 — Upload to Staging

Upload the plate cache files to the staging server. The target directory is /data/gridflock/plate-cache/:

SSH_KEY="/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops" \
  rsync -avz --progress \
  -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=no" \
  ./plate-cache-output/ \
  root@staging-connect-api.forma3d.be:/var/lib/docker/volumes/forma3d_gridflock-data/_data/plate-cache/

Note: The gridflock service loads all plates into memory at startup (~60 MB). Restart the container after uploading for the service to pick up new files, or wait for the next deployment.

12.5 Plate Cache: Verification

After uploading and restarting the service:

  1. Open the storefront configurator and select various grid dimensions — previews should load in under 1 second
  2. Check the service logs for Assembled preview from plate cache: {w}x{h}mm messages (confirms plate-level assembly is active)
  3. If you see Plate cache miss for {w}x{h}mm, falling back to full generation, the plate cache is not loaded — check the PLATE_CACHE_PATH env var or restart the container
  4. Verify dimension normalization: requesting 320×450 and 450×320 should produce the same preview

Expected service startup log:

Plate cache loaded: 200 plates, 41.2 MB

12.6 Plate Cache: Performance Comparison

Approach Cache files Cache size Supports 1 mm resolution? Preview response time
Plate-level cache ~268 ~60 MB Yes, any resolution 10–100 ms (assembled)
No cache (fallback) 0 0 Yes 12–30 seconds

Generation time: ~268 files in 2–5 minutes via scripts/populate-plate-cache.ts.

12.7 Plate Cache: Troubleshooting

Problem Cause Fix
Command "tsx" not found tsx is not installed pnpm add -D tsx
Cannot find module '@forma3d/gridflock-core' tsx can't resolve workspace path aliases Add --tsconfig tsconfig.base.json flag before the script path
Service logs Plate cache not available Plate cache directory doesn't exist or is empty Upload plate files to /data/gridflock/plate-cache/
Service logs Plate cache loaded: 0 plates Directory exists but contains no .stl files Re-run populate-plate-cache.ts and re-upload
Previews still take 12–30 seconds Plate cache not loaded; service falling back to JSCAD Check startup logs for plate count; restart container after uploading
Permission denied (publickey) on upload SSH key not provided or wrong path Set SSH_KEY env var

12.8 Architecture Reference


Quick Reference

URLs

Service URL
Shopify Test Store https://forma3d-test.myshopify.com/admin
Forma3D Staging API https://staging-connect-api.forma3d.be
Forma3D Staging Web https://staging-connect.forma3d.be
SimplyPrint https://simplyprint.io
Sendcloud https://panel.sendcloud.sc

Test Credit Cards

Type Number
Visa (success) 4242 4242 4242 4242
Visa (decline) 4000 0000 0000 0002
Mastercard 5555 5555 5555 4444

API Endpoints

Endpoint Purpose Auth
GET /health System health check Public
GET /api/v1/orders List orders API key/session
GET /api/v1/product-mappings List product mappings API key/session
POST /api/v1/product-mappings Create product mapping API key/session
GET /api/v1/print-jobs List print jobs API key/session
POST /api/v1/storefront/grid-checkout Create draft order for custom grid App Proxy HMAC

Webhook Endpoints

Endpoint Purpose
POST /api/v1/webhooks/shopify Receive Shopify order webhooks
POST /webhooks/simplyprint Receive SimplyPrint job status webhooks
POST /webhooks/sendcloud Receive SendCloud parcel status webhooks

Admin Endpoints (require INTERNAL_API_KEY)

Endpoint Purpose
GET /api/v1/admin/shopify/backfill/status Check Shopify backfill status
POST /api/v1/admin/shopify/backfill/trigger Manually trigger Shopify backfill
POST /api/v1/admin/shopify/backfill/reset Reset backfill watermark
POST /api/v1/admin/print-jobs/:id/force-status Force print job to terminal status
GET /api/v1/admin/print-jobs/stuck Get stuck print jobs
POST /api/v1/admin/print-jobs/stuck/check Manually trigger stuck job check
GET /api/v1/admin/print-jobs/stuck/threshold Get stuck job alert threshold
PATCH /api/v1/admin/print-jobs/stuck/threshold Set stuck job alert threshold

Part 13: Inventory & Stock Management

Forma3D.Connect supports a hybrid fulfillment model: products can be pre-printed to stock and consumed when orders arrive, reducing fulfillment time. This part covers configuring, testing, and troubleshooting the inventory system.

13.1 Concepts

Concept Description
Stock Management Enabled per product by setting minimumStock > 0 on a product mapping
Print-to-Order Default mode — prints are created on demand when an order arrives
Hybrid Fulfillment When stock is available, units are consumed from shelf first; remaining units are printed on demand
Stock Replenishment A scheduled background job that pre-prints items to maintain minimumStock levels
Stock Batch A group of STOCK-purpose print jobs created during a single replenishment run
Inventory Transaction An immutable audit record of every stock change (consume, produce, adjust, scrap)

13.2 Enable Stock Management on a Product

Use the inventory API to configure a product mapping for stock management:

API_KEY="your-api-key"
API_URL="https://staging-connect-api.forma3d.be"
MAPPING_ID="your-product-mapping-id"

# Enable stock management with minimum stock of 5
curl -X PUT "$API_URL/api/v1/inventory/stock/$MAPPING_ID/config" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "minimumStock": 5,
    "maximumStock": 50,
    "replenishmentPriority": 5,
    "replenishmentBatchSize": 1
  }'
Field Required Default Description
minimumStock Yes Target stock level. Set to 0 to disable stock management.
maximumStock No null Upper cap (informational — replenishment stops at this level)
replenishmentPriority No 0 Higher values are replenished first
replenishmentBatchSize No 1 Print jobs per replenishment batch

13.3 Verify Stock Levels

# List all stock-managed products
curl "$API_URL/api/v1/inventory/stock" \
  -H "X-API-Key: $API_KEY" | jq '.'

# Expected response:
# [
#   {
#     "productMappingId": "pm-123",
#     "productName": "3D Benchy - Green",
#     "sku": "BENCHY-GREEN-001",
#     "currentStock": 0,
#     "minimumStock": 5,
#     "deficit": 5,
#     "pendingBatches": 0,
#     ...
#   }
# ]

Products with deficit > 0 will be scheduled for replenishment when the scheduler is enabled.

13.4 Manually Adjust Stock

Use the adjust endpoint to add or remove stock manually (e.g., after physically counting inventory):

# Add 10 units (positive adjustment)
curl -X POST "$API_URL/api/v1/inventory/stock/$MAPPING_ID/adjust" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 10, "reason": "Initial stock from manual count"}'

# Remove 3 units (negative adjustment)
curl -X POST "$API_URL/api/v1/inventory/stock/$MAPPING_ID/adjust" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity": -3, "reason": "Correction after recount"}'

13.5 Scrap Defective Stock

curl -X POST "$API_URL/api/v1/inventory/stock/$MAPPING_ID/scrap" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 1, "reason": "Layer shift defect"}'

13.6 View Transaction History

Every stock change (consumption, production, adjustment, scrap) is recorded in the transaction ledger:

curl "$API_URL/api/v1/inventory/stock/$MAPPING_ID/transactions?limit=20&offset=0" \
  -H "X-API-Key: $API_KEY" | jq '.transactions[] | {transactionType, quantity, direction, notes, createdAt}'

13.7 Enable Stock Replenishment

Stock replenishment is controlled by environment variables. Add these to the Azure DevOps forma3d-staging variable group:

Variable Default Description
STOCK_REPLENISHMENT_ENABLED false Set to true to enable the scheduler
STOCK_REPLENISHMENT_CRON */15 * * * * Evaluation interval (default: every 15 minutes)
STOCK_REPLENISHMENT_MAX_CONCURRENT 5 Max concurrent STOCK-purpose print jobs
STOCK_REPLENISHMENT_ORDER_THRESHOLD 3 Skip replenishment when ORDER-purpose queue exceeds this

After deployment, verify the replenishment status:

curl "$API_URL/api/v1/inventory/replenishment/status" \
  -H "X-API-Key: $API_KEY" | jq '.'

# Expected response:
# {
#   "enabled": true,
#   "activeStockBatches": 0,
#   "activeStockJobs": 0,
#   "maxConcurrentStockJobs": 5,
#   "productsNeedingReplenishment": [...]
# }

13.8 Testing the Hybrid Fulfillment Flow

  1. Configure a product with stock: Set minimumStock: 5 on a test product mapping
  2. Add stock manually: Adjust stock by +5 using the adjust endpoint
  3. Place a test order for that product on the Shopify test store
  4. Verify stock consumption: Check the event log for a CONSUMED transaction — the order should skip print job creation if stock was available
  5. Verify stock decrement: The stock level should decrease by the quantity ordered
  6. Test partial fulfillment: Adjust stock to 1, then order quantity 3 — verify 1 unit consumed from stock + 2 print jobs created

13.9 Testing Stock Replenishment

  1. Enable replenishment: Set STOCK_REPLENISHMENT_ENABLED=true and deploy
  2. Configure a product with minimumStock: 3 and currentStock: 0
  3. Wait for the scheduler (or check logs for StockReplenishmentService activity)
  4. Verify StockBatch creation: A stock batch with STOCK-purpose print jobs should appear
  5. Simulate print completion: Complete the print job in SimplyPrint
  6. Verify stock increment: Check that currentStock increased after the batch completed
  7. Verify transaction: A PRODUCED transaction should appear in the history

13.10 Troubleshooting Inventory

Symptom Likely Cause Resolution
Stock not consumed for an order Product minimumStock is 0 (stock management disabled) Set minimumStock > 0 via the config endpoint
Stock not consumed despite currentStock > 0 Race condition — another order consumed the stock first Check transaction history; this is expected behavior
Replenishment not running STOCK_REPLENISHMENT_ENABLED is false or env var not deployed Verify replenishment/status endpoint shows enabled: true
Replenishment skipped Order queue exceeds threshold, or already at maxConcurrentStockJobs Check replenishment/status for activeStockJobs; reduce ORDER_THRESHOLD
Stock adjust returns 404 Product mapping ID doesn't exist for the current tenant Verify the mapping ID via /api/v1/product-mappings
Stock adjust returns 400 Negative adjustment would reduce stock below 0 Check currentStock before adjusting
Scrap returns 400 Trying to scrap more units than available Reduce scrap quantity to match currentStock
Print job has purpose: STOCK but no stockBatchId Orphaned stock job — batch may have been manually deleted Check event logs; this is a data integrity issue
StockBatch stuck in IN_PROGRESS One or more print jobs in the batch haven't completed Check individual print jobs in the batch via SimplyPrint

13.11 Inventory API Reference

Endpoint Method Purpose
/api/v1/inventory/stock GET List all stock-managed products with levels
/api/v1/inventory/stock/:id/config PUT Configure stock management for a product
/api/v1/inventory/stock/:id/adjust POST Manually adjust stock (positive or negative)
/api/v1/inventory/stock/:id/scrap POST Scrap defective units
/api/v1/inventory/stock/:id/transactions GET View transaction history
/api/v1/inventory/replenishment/status GET Check replenishment scheduler status

13.12 Architecture Reference

For detailed technical background on the inventory system, see:


Revision History:

Version Date Author Changes
1.0 2026-01-19 Documentation Initial guide for real-world testing
1.1 2026-01-20 Documentation Added Part 7: AI-Assisted Development with MCP Servers and CLIs
1.2 2026-01-22 Documentation Added Part 5b: Webhook and Backfill Configuration
1.3 2026-01-28 Documentation Updated Section 1.3: Shopify app creation now uses Dev Dashboard (legacy custom apps deprecated Jan 2026)
1.4 2026-01-28 Documentation Added OAuth as recommended option; created prompt-shopify-oauth.md for OAuth implementation
1.5 2026-02-03 Documentation Added Protected Customer Data Access setup; documented OAuth-first API strategy
1.6 2026-02-04 Documentation Added Part 8: Shopify Theme Deployment with installation and configuration instructions
1.7 2026-02-04 Documentation Added Azure DevOps pipeline variable sections to SimplyPrint (2.6) and Sendcloud (3.6) parts
1.8 2026-02-04 Documentation Clarified SimplyPrint webhooks are required for terminal states; added openssl rand -hex 32 for generating webhook secret
1.9 2026-02-04 Documentation Added stuck print job troubleshooting, force status feature, and admin endpoints for print job management
1.10 2026-02-05 Documentation Clarified Sendcloud test mode is account-level (not a separate integration); clarified "Sendcloud API" is the correct integration for direct API access; added webhook URL to section 3.3
1.11 2026-02-09 Documentation Added troubleshooting: Sendcloud tracking email missing tracking link; Sendcloud email order number not linkable to Shopify
1.12 2026-02-12 Documentation Added "Managing Shopify App Scopes" section: documented three-place sync (Dev Dashboard, env var, shopify.app.toml), scope update procedure, fulfillment order scope requirements, and 403 troubleshooting
1.13 2026-02-18 Documentation Added section 8.9: Service Point Delivery Setup & Testing; added Part 9: Grid Product Configuration in Shopify; added Part 10: GridFlock Pipeline — How Custom Grid Orders Are Processed
1.14 2026-02-23 Documentation Added Part 11: Shopify App Proxy & Storefront Security — HMAC validation guard, rate limiting, theme proxy path config, troubleshooting
1.15 2026-02-26 Documentation Added section 10.4b: Dimension & Pricing Settings — configurable max dimension (gridflock.max_dimension_mm), grid pricing SystemConfig keys, runtime update instructions; updated troubleshooting table
1.16 2026-02-27 Documentation Added Part 12: STL Preview Cache Pre-Population — offline generation and upload workflow, CLI arguments, verification steps, performance reference
1.17 2026-02-27 Documentation Part 12: Added prerequisites section (tsx install), --tsconfig tsconfig.base.json flag to all commands, troubleshooting table for common errors
1.18 2026-02-27 Documentation Part 12: Documented two-layer concurrency model, pnpm nx build gridflock-core prerequisite for worker threads, fixed plate-worker.js path resolution
1.19 2026-03-01 Documentation Part 12: Rewrote for plate-level cache system (200 base plates, ~41 MB, 10–100 ms assembly). Legacy full-preview cache documented as optional backward-compatible path. Updated input validation limits to 100–1000mm in section 11.6.
1.20 2026-03-01 Documentation Part 12: Removed legacy full-preview cache section (16,471 files / ~32 GB deleted from staging). Preview cascade simplified to two tiers. Removed references to deleted scripts (populate-preview-cache.ts, upload-preview-cache.sh).
1.21 2026-03-08 Feature Added Part 13: Inventory & Stock Management — stock configuration, replenishment testing, hybrid fulfillment verification, API examples, troubleshooting. Updated overview test flow and environment variables for stock replenishment.