Translate Shopify B2B order quantities into the pack units a warehouse or ERP expects: each, inner, case, pallet, or a merchant-defined unit.
PackBridge is a backend-first Shopify embedded app. It does not enforce quantity rules at checkout. It processes orders after creation, resolving merchant-defined pack rules and delivering normalized quantities via signed webhook payloads.
PackBridge is a production-shaped Shopify app built around B2B order operations:
- Embedded Shopify admin app built with React Router, TypeScript, Prisma, PostgreSQL, and Polaris web components.
- Post-order normalization pipeline for company, location, product, and variant-specific pack rules.
- CSV import path for merchant-maintained rules, including validation feedback before records are written.
orders/createwebhook processing with event-level audit trails and signed outbound delivery.- Public-safe launch docs, screenshots, deployment notes, and App Store review prep under
docs/.
This repo is intended as a work sample for Shopify app builds, webhook-heavy backend work, and practical B2B workflow automation.
If you are reviewing this as a work sample, start with
docs/github-portfolio.md. It summarizes the
business problem, architecture, proof points, screenshots, and public-safe
Upwork/job-application positioning.
The app includes the core embedded admin surface, pack-rule management, CSV import, Shopify order ingestion, normalization jobs, signed downstream delivery, and review-safe public routes. Shopify scopes are read-only: read_orders, read_products, read_companies, and read_customers.
Live deployment details are intentionally kept out of the public README. See docs/deploy.md for the deployment model and environment checklist.
- Merchant installs the app. PackBridge syncs companies, locations, and product variants.
- Merchant defines pack rules — scoped by company, location, product, or variant — via the admin UI or CSV import.
- A B2B order arrives via Shopify's
orders/createwebhook. - PackBridge resolves the most specific matching rule for each line item, validates divisibility, and translates
input_quantity→output_quantityin the downstream unit (CASE, INNER, PALLET, etc.). - A signed JSON payload is POSTed to the merchant's configured webhook endpoint.
- Every step is logged in a full audit trail visible in the Jobs admin.
Shopify webhook (orders/create)
→ authenticate + extract B2B context
→ normalizer: resolve rules, validate line items, record events
→ outbound: assemble payload, HMAC sign, POST with retry
→ OutboundDelivery audit row
Stack: React Router v7 · TypeScript · Prisma 6 · PostgreSQL · Shopify Admin GraphQL API · Polaris web components
app/
components/ Shared React components (RuleForm)
lib/ Pure helpers (GID conversion)
routes/
app._index.tsx Dashboard
app.rules.* Rules CRUD + CSV import
app.jobs.* Normalization jobs list + detail
app.settings.tsx Webhook URL, signing secret, defaults
app.tsx Layout + billing gate + nav
auth.* Shopify OAuth
webhooks.* Webhook handlers (orders/create, compliance, etc.)
services/
billing.server.ts Subscription check + gate
csv-importer.server.ts CSV parse, validate, execute import
normalizer.server.ts Order → events pipeline
orders.server.ts GraphQL order fetch with B2B context
outbound.server.ts Payload build, HMAC sign, POST + retry
rule-resolver.server.ts Score, resolve, validate line items
shop-settings.server.ts Signing secret bootstrap + rotate
sync.server.ts Company/variant/shop sync from Admin API
shopify.server.ts App config, billing plan, afterAuth hook
db.server.ts Prisma client singleton
prisma/
schema.prisma 10 models (Session, Shop, Synced*, PackRule,
NormalizationJob/Event, OutboundDelivery,
RuleImportJob)
migrations/ 2 migrations (init + phase 3 fields)
docs/
app-store-listing.md App Store submission copy
app-store-readiness.md Launch/review gates and proof checklist
deploy.md Production deployment notes
public/
privacy.html Privacy policy
- Node.js >= 20.19
- PostgreSQL database
- Shopify CLI
# Clone and install
git clone https://github.com/morgan-coded/packbridge.git
cd packbridge
npm install
# Configure environment
cp .env.example .env
# Fill in: SHOPIFY_API_KEY, SHOPIFY_API_SECRET, DATABASE_URL, SHOPIFY_APP_URL
# Run migrations and generate Prisma client
npm run setup
# Start development
npx shopify app dev --tunnel-url https://your-tunnel-url:8080| Script | Description |
|---|---|
npm run dev |
Start dev server via Shopify CLI |
npm run build |
Production build |
npm run setup |
Generate Prisma client + run migrations |
npm test |
Run Vitest |
npm run test:watch |
Vitest in watch mode |
npm run typecheck |
React Router typegen + TypeScript check |
npm run lint |
ESLint |
PackBridge is read-only:
read_orders— process B2B ordersread_products— mirror variants for pack rulesread_companies— resolve company-scoped rulesread_customers— read purchasing entity on B2B orders
No write scopes.
$49/month launch pricing with a 14-day free trial for new installs. The legacy/pro $99/month plan remains accepted for active subscriptions. Billing is gated at the app layout level in production — every /app/* page checks for an active subscription before rendering.
Local/dev environments skip the billing gate so embedded previews work before public distribution. Set NODE_ENV=production on the production host to enforce billing. During pre-launch App Store review or dev-store validation, set SHOPIFY_BILLING_TEST=true so the production app still requires billing but requests Shopify test charges.
Rules are resolved with a most-specific-wins strategy:
| Priority | Scope | Score |
|---|---|---|
| 1 | Company + location + variant | 8 |
| 2 | Company + variant | 6 |
| 3 | Company + location + product | 7 |
| 4 | Company + product | 5 |
| 5 | Company only | 4 |
| 6 | Global variant | 2 |
| 7 | Global product | 1 |
Ties are broken by updatedAt descending. Rules respect active, effectiveStart, and effectiveEnd.
- warn — order proceeds, flagged for review
- hold — order held from downstream release until reviewed
- normalize_only — translation emitted with warnings if not divisible
When a B2B order is normalized, PackBridge POSTs a signed JSON payload:
Headers:
Content-Type: application/jsonX-PackBridge-Signature: sha256=<HMAC hex digest>X-PackBridge-Delivery-Id: <delivery cuid>
Signature verification (example in Node.js):
const crypto = require('crypto');
function verify(body, signature, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(body).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}The signing secret is visible in the app's Settings page. Retries: up to 3 attempts at 1s, 5s, 15s intervals.
Vitest suite covering rule resolution, outbound signing, normalization status, form parsing, and direct-launch redirects:
- rule-resolver.test.ts — priority ordering, date filtering, active flag, validateLineItem
- normalizer.test.ts — computeOverallStatus (hold/completed/mixed/empty)
- outbound.test.ts — HMAC signing, payload hashing, determinism
- form-values.test.ts — Shopify form-value normalization and rule scoping
- direct-launch.server.test.ts — embedded-launch redirect handling
npm test- Set
NODE_ENV=productionon your host - Set
DATABASE_URLto your production PostgreSQL connection string - Set
SHOPIFY_API_KEY,SHOPIFY_API_SECRET,SHOPIFY_APP_URL - Run
npm run setupto apply migrations - Run
npm run build && npm start
See docs/deploy.md for the PackBridge runbook and the Shopify deployment docs for hosting options.
Proprietary. All rights reserved.