Methodology

How Folio captures and verifies POs.

This page documents the technical flow Folio uses to collect a buyer's purchase order PDF after Shopify checkout and link it to the correct order, without requiring Shopify Plus.

Architecture overview

Folio runs as a Cloudflare Worker at folio.maselabs.com. It serves a static marketing site and exposes API routes under /api/*. Order and document data are stored in a Neon Postgres database accessed over HTTP via the @neondatabase/serverless driver. The Shopify app extension renders on the Thank You and Order status pages.

Step 1: Order creation webhook

When a store installs Folio, Shopify registers an orders/create webhook pointing to /api/webhooks/orders-create. Each request is verified using HMAC-SHA256 with the app's shared secret before any database work.

The webhook payload includes checkout_token, payment_gateway_names, and the order ID. Folio only creates a po_orders record when checkout_token is present and one of the payment gateway names normalizes to Purchase Order (case- and punctuation-insensitive). This ensures the upload block only appears for manual PO orders.

Step 2: Thank You / Order status page extension

Folio's purchase.thank-you.block.render extension mounts after checkout. It reads the checkout token from Shopify's checkoutToken signal (which may populate after the extension first renders) and polls /api/order-status with a signed session token until the order is confirmed as a purchase order, the buyer used a different payment method, or the maximum retry limit is reached.

Step 3: PDF upload

When the buyer selects a PDF, the extension validates the file client-side (.pdf extension, application/pdf type, %PDF- magic bytes, ≤ 5 MB). On upload, it sends the file and checkout token to /api/uploads with the same session token for authentication.

The Worker re-validates the PDF server-side, enforces the 5 MB limit by bounding the request body stream, and updates the po_orders row matching the shop and checkout token, setting status = 'received' and storing the file bytes in document_data.

Step 4: Merchant inbox

The embedded app at /app loads the merchant's PO orders via /api/orders (paginated, cursor-based). It shows the order name, document filename, size, and status badge. For received orders, the merchant can download the PDF from /api/orders/:id/document and mark it reviewed via /api/orders/:id/review, which sets status = 'reviewed'.

Step 5: Readiness verification

/api/readiness reports three checks: database round-trip, schema presence (po_orders table), and whether at least one real PO order has been uploaded and marked reviewed. The endpoint returns ready: true only when all three pass.

Data model

The po_orders table (see migrations/0001_init.sql) stores one row per PO order per shop, keyed by (shop, checkout_token) with a unique constraint on (shop, order_id). Columns include the document name, size, binary data, and status (missing | received | reviewed). An index on (shop, status, updated_at) supports the merchant inbox query.

Security boundaries

Why not Shopify Files?

Folio stores PDFs in Postgres (BYTEA) rather than Shopify Files because: (1) it keeps the document with its order record atomically, (2) it avoids additional API calls and scope requests, (3) the 5 MB limit fits comfortably in a row, and (4) it satisfies data residency and deletion requirements without a second storage system.

Limits and constraints

Test procedure

  1. Install Folio on a regular-plan Shopify dev store (not Plus).
  2. Add a manual payment method named exactly Purchase Order.
  3. Place a test order using that payment method.
  4. On the Thank You page, upload a valid PDF.
  5. Open the embedded app (/app in Shopify Admin) and confirm the order shows Received with the filename.
  6. Download the PDF to verify integrity.
  7. Mark the order Reviewed.
  8. Confirm /api/readiness returns ready: true.