MannaHarvest Order Profit Tracker — Shopify → profit-per-order sync
Built for MannaHarvest.com, an e-commerce store that needed to know its actual profit per order, not just revenue. Orders sync automatically from Shopify, staff enter product and shipping costs through a small web app, and profit per order falls out of a database view — no spreadsheet, no manual calculation.
Architecture
Three services in Docker Compose on a single VPS:
- Caddy — reverse proxy, automatic HTTPS.
- Fastify (Node.js) — the API and a small vanilla-JS frontend, no build step.
- PostgreSQL — orders, line items, and an
order_financialsview that cascades cost and revenue from line item → product → order → profit automatically.
Keeping orders in sync
Shopify webhooks (orders/create, orders/updated, orders/cancelled)
push changes in real time, each verified via HMAC signature before being
processed. Webhooks get missed sometimes — delivery failures, downtime —
so a periodic reconciliation job re-scans recent orders every 4 hours as a
fallback. Together they keep the full order history in sync: the webhook
path handles the real-time case, reconciliation catches anything that
slips through.
Fee sync: no webhook, so three layers of retry
Shopify doesn’t emit a webhook for payment-processing fees — that data only shows up once the charge settles on Shopify’s side, which doesn’t happen at the same moment the order is created. One lookup attempt isn’t enough, so fee sync is three layers deep:
- An immediate best-effort lookup fires the moment
orders/createcomes in. - If the charge hasn’t settled yet, the order is flagged and a targeted retry job checks it hourly, up to five times.
- An unlimited, uncapped sweep runs every 6 hours as the fallback that never gives up on an order.
All three jobs also fire once on process startup, not only on their
interval — setInterval timers reset on every deploy, so without that,
redeploying during active development could leave orders fee-less far
longer than intended. (A real bug, caught and fixed.)
Cost entry with carry-forward
Shopify doesn’t store real product costs for this store, so costs are entered by hand through an Order Detail view. To avoid re-typing the same cost on every order, once a cost is confirmed for a SKU it carries forward and pre-fills on the next order with that SKU — flagged for re-confirmation rather than silently trusted, since costs do change.
Security
- Passwords hashed with bcrypt; sessions use an
httpOnly+Secure+SameSite=Strictcookie holding a signed JWT, not readable by page JavaScript. - Every
/api/*route requires authentication — no public sign-up. - Login is rate-limited.
- Fastify runs with
trustProxyenabled, since Caddy sits in front — without it, every request looks like it’s coming from Caddy’s internal IP, which silently breaks per-visitor rate limiting.
Why this stack
- Fastify over a heavier framework: the API surface is small (sync, auth, a handful of report endpoints) — no need for more structure than that.
- A plain Postgres view for profitability: cost/revenue cascade is a pure data-modelling problem; a SQL view keeps the numbers always correct and avoids duplicating the calculation in application code.
- Docker Compose on a single VPS: three services, modest traffic — no
need for anything more elaborate than
docker compose up.