Case study
An operations and finance dashboard for e-commerce
Implementation case study. One application instead of eight browser tabs, a spreadsheet, and the owner's memory.
Context
A UK online retailer selling beauty and hairdressing equipment across two channels (its own Shopify store and eBay), supplied by a B2B wholesaler in Poland, shipping through Royal Mail Click & Drop, and running customer service through Gmail and eBay messages.
Each of those tools has its own dashboard and its own version of the truth. None of them knows about the others:
- Shopify knows what the customer ordered — but not what the goods cost or whether they are in the UK warehouse.
- eBay knows about its own order — but masks the buyer’s email address, so buyer enquiries never reach any shared inbox.
- Royal Mail knows a label was paid for — but not which order it belongs to.
- The supplier emails a proforma, publishes the invoice in a B2B portal, and physical goods-in is recorded by someone else in a spreadsheet.
- Real profit exists nowhere: producing it means combining purchase price, freight from Poland, domestic shipping, channel fees and returns.
The result: decisions made on gut feel, processes living in one person’s head, mistakes found by accident (or by the customer), and every question like “what did we actually make on this category” costing half a day of spreadsheet-stitching.
Off-the-shelf e-commerce analytics tools were considered and deliberately rejected — none of them handles backorders, a packing queue, customer correspondence or purchase documents from Gmail. They were tools for looking at sales, not for running a business.
Goals
Build a single source of truth that:
- merges data from every channel into one model of an order, a customer and a product,
- walks people through the process — from the customer’s order, through purchasing from the supplier, to packing and dispatch — rather than only reporting after the fact,
- shows real margin, not turnover,
- never does anything irreversible without a human — because the system touches customers’ money, their personal data and outbound correspondence.
Architecture
Stack: Next.js 15 (App Router, TypeScript) · React 19 · PostgreSQL 16 · Prisma 6 · a node-cron worker · Docker Compose · Claude model integration (Anthropic SDK) for customer-service assistance.
Core principle: raw → core → presentation.
Every data source lands in the database first as a raw payload (raw_*), and only then is normalised
into the domain model (orders, order_items, customers, shipments, purchase_orders,
conversations, costs_*). The interface never queries a live API — it reads only from the
database.
Three practical consequences of that decision:
- The app works when an integration breaks. An expired token degrades data freshness, not the whole dashboard.
- Changing logic doesn’t require re-fetching data. A fixed parser is re-run over payloads that were kept.
- Views are fast and predictable, because they are plain SQL queries rather than a chain of calls into third-party APIs and their rate limits.
The sync layer is a worker with a job registry (11 independent jobs: Shopify orders, inventory, marketing consent, payment disputes, eBay orders, eBay messages, Gmail, Royal Mail documents, supplier proformas and invoices). Each job has its own state and its own kill switch — one failure doesn’t stop the others, and nothing ever guesses the next status. A separate fast loop polls the inbox every 2 minutes under its own lock, so a long order sync can’t starve customer service.
What the system does — modules
1. Orders and fulfilment
A unified order list across both channels; an order page with line items, history, correspondence, tracking and serial numbers. Operational filters answer “what needs attention today”, not just “what happened”.
2. Purchasing and backorders
Quantities split into needed / confirmed with supplier / received / still to buy. The purchase evidence chain is separated by source — the proforma arrives by email, the invoice lives in the supplier’s B2B portal, and physical goods-in is confirmed by the warehouse sheet. The system enforces that no document pretends to be another one: an invoice does not mean the stock is on the shelf.
3. Packing and dispatch queues
Separate screens for the owner (what is blocking dispatch: no label, no QR code, no courier confirmation) and for the packer (a safe “today” queue plus the backlog). Click & Drop documents are parsed from payment-confirmation emails and tied to a specific order by the carrier’s official identifier from its API — not by guessing from the email body.
4. Customer service inbox
Gmail threads and eBay buyer messages in one place, alongside full order context. Statuses, snoozing, tags, internal notes, templates with substituted variables, response-time metrics.
This was the highest-return module: eBay messages previously never reached any shared inbox at all, because the platform masks the buyer’s address. On top of that, 95% of traffic in that channel is platform automation — a filter that recognises genuine buyer questions was a precondition for the channel being serviceable at all.
5. LLM-assisted replies
The model drafts a suggested reply from the thread, the order, tracking data and the company’s own templates. The model neither sends nor saves anything. Its suggestion returns to the editor as plain text and must pass exactly the same checks as text typed by hand. Tagging rules, by contrast, are deliberately deterministic rather than model-driven — at this correspondence volume, a model in the automatic path would add failure modes to something that simply has to work.
6. Finance: costs and CM2 margin
Fixed and variable costs, real purchase prices from B2B invoices, freight from Poland calculated by weight, real domestic shipping costs from carrier statements, channel fees. The output: contribution margin after variable costs per order, per customer and per channel — the number that says whether a sale made sense, rather than how much money moved through the account.
7. Forecasting, retention, Customer 360
Cash-flow forecasting, customer lifetime value, purchase history, cross-channel customer identities, and a record of the legal basis for marketing consent.
8. Data quality and reconciliation
A dedicated job compares every order in the database against its current state in Shopify — gross amount, VAT, line count — and reports drift. This module exists because of a specific incident: a discrepancy in an edited order was caught by a human eye, since nothing in the system compared stored totals against the source. Today a machine compares them on every run.
9. Roles and GDPR
Login, roles, audit log. The operational account sees orders, the packing queue and dispatch status, but not finances, banking, forecasts or customer value. GDPR Articles 15 and 17 are implemented explicitly: one-click export of a customer’s complete data, and redaction rather than deletion — identifying data disappears while order amounts, numbers and dates remain, because tax records have to survive an erasure request.
The design rules that mattered most
These decisions are more interesting than the feature list — they are what separates a tool you can trust from a good-looking dashboard.
A human approves everything that leaves the building. Only a previously saved draft can be sent to a customer; there is no “write and send in one click” path. Sending is disabled by default behind an environment switch. On eBay the system doesn’t reply at all and links out to the platform — because eBay has no drafts and the message would go out instantly. Tests enforce this and fail if anyone adds a path around it.
Automation may only tighten, never loosen. A rule can raise a ticket’s priority, never lower it. Marketing-consent sync never reverses a manually recorded objection. Re-importing an old message never reopens a case a human closed.
Idempotency as a requirement, not an aspiration. All thread metrics are recomputed from source data rather than incremented. A repeat import, a history backfill and a manual operator action all produce exactly the same result. That makes “just run it again” a safe answer to most problems.
Every number states where it came from. Each abbreviation and column header carries a tooltip with its definition, its data source and a caveat about what the number excludes. It turned out that most misreadings came not from misunderstanding a label but from a wrong assumption about provenance — for instance, that a channel fee was an actual charge rather than an estimate. A tooltip describing an outdated calculation is worse than none, so keeping the glossary current is part of the definition of done.
A warning rather than a hard block where reality is messier than the rule. When an email sender isn’t the order’s owner, the system warns and gates the button behind explicit confirmation instead of blocking outright. A husband writes about his wife’s order; a customer using a second address is still the same customer. But without that warning, a template would substitute the order owner’s personal details into a message addressed to a stranger. That is a data leak waiting to happen, and it surfaced in user testing, not in code review.
One writer per record. The dashboard only reads inventory levels; writes belong to a separate automation. Two systems writing the same stock figure drift apart silently, and that kind of bug shows up as a missing parcel.
Business impact
For the owner
- real margin instead of turnover — per order, per customer, per channel,
- one place showing what is blocking dispatch and what needs to be purchased,
- drift between systems reported by a machine, not by a customer,
- decisions (such as what is worth selling in which channel) based on a number, not a hunch.
For customer service
- one inbox instead of two platforms plus Gmail search,
- full order context next to the message — no switching between panels,
- templates and drafted suggestions that cut reply preparation time,
- the eBay channel became serviceable at all,
- measurable response time, reported as a median rather than a mean skewed by one forgotten thread.
For the packer
- a “what do we pack today” queue in which a parcel cannot go out without complete paperwork,
- access limited to what the job requires — no visibility into company finances.
For legal compliance
- access and erasure requests handled by one tool instead of manually searching eight tables,
- a record of the legal basis for marketing consent,
- an audit log for sensitive operations.
Technical scale
| Application screens | 21 |
| Data models | 51 |
| Database migrations | 32 |
| Independent sync jobs | 11 |
| Integrated external systems | Shopify Admin, eBay (REST + legacy Trading API), Gmail, Royal Mail Click & Drop, Google Sheets, supplier B2B portal, banking, Anthropic API |
| Application code | ~29,000 lines of TypeScript |
How the work was run
The project was delivered incrementally: each stage shipped independently, behind its own kill switch, and could be turned off without touching the rest. Design decisions and the quirks of specific APIs are documented in the repository, next to the code — including post-incident write-ups. As a result, the knowledge of why something is built the way it is doesn’t live in anyone’s head.
Deliberate scope limits: the system runs locally, on the owner’s machine, with a database reachable only from that computer. Exposing it to the internet is a separate stage with its own requirements (a full user model, HTTPS, gated access, backups) and was not done “while we were at it”, because the database holds the contents of customer correspondence.
What this demonstrates
That a small e-commerce business can have an operating system shaped exactly around its own process — backorders, packing queue, real margin — instead of bending the process to fit whatever an off-the-shelf app happens to offer. And that in a system like this the hard part isn’t writing the screens; it’s treating the data honestly: knowing where every number comes from, refusing to let automation make a decision that belongs to a human, and building the mechanism that raises its hand when something has drifted.