Fashion / E-commerce
How I Replaced an Agency for an 8-Digit Fashion Brand on Shopify Plus — And Built a Custom Order, Shipping & RFID Warehouse System Over Several Years
A deep-dive into replacing off-the-shelf Shopify apps with a custom order management, multi-carrier shipping (UPS, DHL, FedEx) and RFID warehouse system — built iteratively for an 8-digit fashion brand on Shopify Plus.
TL;DR
An 8-digit revenue fashion brand on Shopify Plus was drowning in a mix of half-working apps and manual processes. They had already hired large web agencies — and left. Not because the work was bad, but because they were one client among many. What they needed was a single partner, always reachable, who would build something truly tailored to how they actually operated.
I’ve been that partner for several years. Over that time, we went from 1 shipping station running barcode scanners to 6 shipping stations with a full custom RFID warehouse system — including custom EPC generation, native ZPL label printing, direct UPS/DHL/FedEx API integration, and auto-built RFID antennas with components sourced directly from China.
All of this was designed and shipped years before AI coding assistants existed. The architecture was the product of reading datasheets, calling carrier support lines, and making decisions I could defend.
This is a case study about what happens when a client buys a person instead of a vendor — and why building small first and growing with the business beats monster-sized day-one projects, every time.
The starting point — a Shopify Plus store that had outgrown its tooling
The client is a fashion brand I’m keeping anonymous. Shopify Plus, operating across multiple EU markets, high order volume, and a revenue profile that put them well past the point where off-the-shelf apps can handle the complexity of real operations.
On paper, everything looked fine. In practice, here’s what the day-to-day looked like:
- A patchwork of Shopify apps, each solving one problem and none talking to the others. Inventory in one place, shipping in another, returns handled on a shared spreadsheet.
- Manual steps everywhere. Pickers choosing items by memory and printout. Labels generated one carrier at a time, often by typing the address back into a carrier portal.
- Returns that nobody could really track. In fashion, where return rates routinely hit 30–40%, this isn’t a nuisance — it’s a silent margin killer.
- Inventory drift. What Shopify said was in stock and what the warehouse actually had diverged by the day.
They had already tried the obvious route: hiring a large web agency. Two of them, in fact. Both delivered. Neither stuck.
Why they fired the agencies (and why this matters)
When the client told me why they’d left their previous agencies, they didn’t complain about code quality or deadlines. They said, in plain words:
“We were one client among many. Nobody owned our problem. We needed someone who picks up the phone, knows our operations, and builds things for us — not a generic solution with our logo on it.”
This is the story I hear most often from technical founders and operations leads at mid-market companies. It’s not that agencies are bad. It’s that the economics of agencies make it almost impossible to give a 10–20M EUR client the same attention they give to a 100M EUR one. You get a project manager, a rotating cast of developers, and a Slack channel where answers arrive in 48 hours.
What they wanted was simple:
- One person, reachable, who remembers the project’s history.
- Custom, meaning actually tailored to their operations, not a template with their name pasted in.
- Vertical, meaning someone who would learn how a fashion warehouse actually works — picking flows, return logistics, shipping cut-off times, peak-season chaos.
That’s the brief I walked into. It’s also, not coincidentally, the exact shape of the freelance relationship I’ve spent the last 15 years refining.
The architectural decision that saved the project: start small
Here’s the part most case studies skip. Big agencies pitch big systems. A platform, a portal, a dashboard, a migration. It sounds impressive in a proposal and it kills projects in production.
We didn’t do that.
The first release of the system did one thing: it pulled orders from Shopify into a custom web app and let a single shipping station print the right label with a barcode scanner. That’s it. One station. No RFID. No returns workflow. No multi-carrier logic beyond “which of these three carriers does the order go with”.
It shipped in a few weeks. The immediate result was that manual errors from the shipping desk dropped to near-zero, because the operator didn’t have to re-enter anything — scan, confirm, print.
Then we let the system live in production for months before adding the next piece.
This is the architectural discipline I’d push on any founder reading this. Every additional feature we built later — multi-carrier support, returns intake, RFID inventory, warehouse receiving, periodic stock checks — was added because the client came back with a concrete operational pain, not because someone sketched it on a roadmap two years in advance. Each addition was paid for by the revenue and stability the previous phase had already created.
Years later, the system runs 6 shipping stations and touches every step of post-order operations. But the codebase doesn’t look like a cathedral. It looks like what it is: a system that grew in answer to real problems, one at a time.
The Shopify integration layer
The web app sits outside Shopify — it’s not a Shopify app in the storefront-install sense. It’s a standalone custom system that talks to Shopify through three channels, each chosen deliberately:
- Webhooks for the things that have to be real-time: new orders, paid orders, order edits, refunds initiated in Shopify. Webhook handlers are idempotent and persist the raw payload before any processing happens, so we can replay anything if a downstream step fails.
- REST API for standard CRUD — products, customers, inventory levels. Reliable, well-documented, cached where appropriate.
- GraphQL API for anything where REST would have meant five round-trips. Fetching an order with its line items, fulfillments, and refunds in a single call. GraphQL also gives us much finer control over which fields we hit, which matters once you’re doing thousands of calls a day and API limits become real.
If I had to summarize the rule that keeps this layer sane: never trust a single channel. Webhooks can fail, be delayed, or arrive out of order. A nightly reconciliation job compares the state of orders in the custom system against Shopify, flags drift, and lets an operator fix discrepancies before they become customer complaints.
Multi-carrier shipping: UPS, DHL, FedEx — native APIs, no aggregator
Early on, we discussed using a shipping aggregator (EasyPost, ShipStation, that kind of service). The calculation was simple: an aggregator saves integration work upfront and charges a small fee per shipment. At low volume, this is fine. At thousands of shipments a month across three carriers, the cumulative cost becomes real — and you’ve traded integration effort for vendor lock-in and a layer you can’t debug.
We went native.
Each carrier got its own direct integration with the official API: authentication, rate shopping, label generation, tracking number retrieval, status polling. The code lives behind a ShippingProvider interface, so from the web app’s point of view “print a UPS label” and “print a DHL label” are the same call with a different enum.
The practical payoff:
- Zero per-shipment fees beyond what the carriers themselves charge.
- Direct support channel — when something goes wrong with a label, I’m talking to the carrier, not to an aggregator talking to the carrier.
- Fine-grained control over things like service level selection, declared value, insurance, and customs documents (critical for a brand shipping across EU borders).
Was this more work? Yes, significantly. Would I do it again? For this volume and this client, absolutely. For a smaller merchant, I’d probably recommend an aggregator and move on.
Label generation: writing ZPL by hand (and why)
Shipping labels, packing slips, barcode tags for internal warehouse use — all of them are generated by the web app as raw ZPL (Zebra Programming Language), sent straight to the Zebra industrial printers over the network.
I could have taken an easier path. Generate a PDF, send it to the OS print driver, let the driver handle the rest. Plenty of Shopify apps do exactly that.
ZPL is harder. It’s a text-based printer language where you place each element by absolute coordinates, declare fonts, draw barcodes, and compose the label byte by byte. But the payoff is significant:
- Printer-native speed. A ZPL label prints in milliseconds. PDF-over-driver prints in seconds. Across 6 stations and a peak-season shipping day, this is the difference between a smooth afternoon and a queue.
- Zero OS dependencies. No printer drivers, no Windows spooler, no “is the driver installed on this workstation” support calls. The web app opens a TCP socket and sends bytes.
- Exact control over layout. Label positioning, barcode size, text kerning, logo placement — all deterministic, all versioned in code.
- Internal barcode tags, used for warehouse-internal tracking (bin locations, pickup tickets, shelf labels), are generated the same way. One label-generation pipeline, many use cases.
ZPL is one of those skills you don’t realize is rare until you need it. It’s also one of the clearest signals that a project has been built by someone who knows the operational stack, not just the web stack.
The RFID warehouse system — building hardware and software together
After a few years, the client’s operations had outgrown barcode scanning. Fashion SKUs are numerous, visually similar, and pickers have to be fast. Barcode scanning works, but it’s one-item-at-a-time and requires line-of-sight.
The answer was RFID, and here’s where the project stopped looking like a normal web development engagement.
We built fixed RFID stations at each of the 6 shipping desks. The antennas are positioned under the counter surface, creating a read zone directly over where items are placed during picking, returns intake, or receiving.
The stations were built with off-the-shelf RFID readers and custom antenna work, with components sourced directly from manufacturers in China. This kept the per-station cost at a fraction of what a turnkey industrial solution would have charged, and — importantly — gave us full control over the hardware stack.
Custom EPC generation and RFID label printing
Here’s a part most people don’t realize is a build-from-scratch problem: where do the RFID tag codes come from?
Every RFID tag carries an EPC (Electronic Product Code) — a unique identifier written into the tag’s memory. You don’t buy pre-programmed tags off a shelf; you write them at the moment of tag production. Which means whoever builds the system has to decide:
- The EPC numbering scheme (how codes map to SKUs, variants, production batches).
- Who generates them, when, and where they live in the database.
- How they get written to the physical tags.
In this system, the web app generates EPCs on demand, following an internal numbering scheme designed around the client’s SKU structure. When a new batch of tags is needed, the web app queues up the EPCs and sends them to an RFID-capable industrial printer, which in a single pass:
- Writes the EPC into each tag’s memory.
- Prints the human-readable label on the tag’s adhesive surface (also in ZPL).
- Verifies the tag was written correctly before accepting the next.
The result is a tag-generation pipeline that is fully under the client’s control. There is no third-party tag-encoding service in the loop. No monthly subscription to a label platform. No dependency that could be discontinued, repriced, or broken by an API change.
The Python bridge between hardware and web app
The bridge between the RFID hardware and the web app is a small Python service running on each station. The Python side handles the low-level reader protocol, deduplicates tag reads (RFID readers are enthusiastic — a single tag can be read dozens of times per second), and pushes clean events to the web app’s API over the local network. The web app never talks to the reader directly; it talks to the Python bridge, which means we can upgrade the web app and the hardware stack independently.
What RFID changed operationally
- Picking: the operator places items in the shipping zone and the system confirms, in real-time, that the correct items for the order are present. Wrong item? The UI shows it before the package is closed.
- Returns intake: when a return arrives, the box goes on the RFID desk, the tags are read, and the system automatically matches them to the original order and flags condition-check tasks. No typing, no searching.
- Warehouse receiving: inbound pallets are checked against the PO by passing items over a station, with the system reconciling expected vs. received quantities automatically.
- Periodic inventory checks: the same hardware doubles as an inventory auditing tool — walk a cart of items past the station, get a live count.
A note on timing: this was built before AI coding assistants existed
It’s worth saying plainly, because it changes how this case study should be read in 2026.
When this system was designed and built, there was no ChatGPT, no Claude, no Copilot, no AI pair-programmer of any kind. Every decision in this case study — the three-channel Shopify integration pattern, the native carrier API abstraction, the ZPL pipeline, the EPC generation scheme, the Python bridge architecture, the antenna sourcing — came from reading datasheets, experimenting in production, calling carrier support lines, and being wrong in small ways until I was right.
I’m not opposed to AI tools. I use them daily now, for what they’re good at. But the judgment that decides which pattern to use, when to integrate directly versus through an abstraction, and where the real complexity of a client’s operations actually lives — that judgment isn’t in a model weight. It’s in the years of work that came before.
When a client hires me today, they’re hiring that judgment. The AI is just a typing accelerator.
The tech stack — and why it’s not the point
For the curious: the web app was originally built in CakePHP. Today, on a greenfield project for a similar client, I’d reach for Laravel, React/Inertia on the front-end, and MySQL on the database side.
But the framework is genuinely not the interesting part of this case study. The interesting parts are:
- Deciding what to build first, not eventually.
- Choosing between aggregators and direct carrier integrations based on volume and lock-in, not on what’s easier.
- Treating hardware as part of the system, not as a separate procurement problem.
- Owning the tag-generation pipeline end-to-end, EPC numbering included.
- Writing ZPL by hand when the printer-speed difference matters at peak season.
- Building a bridge layer (Python + RFID) that lets the web app and the hardware evolve independently.
- Running webhooks and reconciliation jobs together, so no single channel is a single point of failure.
These decisions would matter on Laravel. They would matter on Rails. They mattered on CakePHP, which is what we had.
What this client got that an agency couldn’t give
After several years of working together, here’s what the relationship looks like:
- Every line of code in the custom system was written with their specific operations in mind. There is no “generic” part, because there was no template to start from.
- When they call, I pick up. When they have a new constraint — a new carrier, a new country, a new workflow — we discuss it that week, not after a statement-of-work negotiation.
- The system grew with the business. From 1 station and barcode scanners to 6 stations and RFID, all on the same core architecture. No rebuild, no re-platform, no “v2”.
- There is no vendor lock-in. The system runs on infrastructure they control. The code is theirs. If I got hit by a bus tomorrow, another senior engineer could pick it up — it’s not a black box.
When this kind of engagement makes sense for you
If you’re reading this as a founder, operations lead, or CTO, this kind of work is the right fit when:
- Your operations have outgrown what Shopify apps can do, but you’re not large enough to justify an in-house development team.
- You’ve already tried an agency and felt like a number.
- You need someone who will still be there in 3 years, because the system you build now is going to need to grow.
- You understand that “custom” isn’t a license to over-engineer — it’s a license to build exactly what you need, nothing more, and add the rest when it becomes real.
A short call to understand your context, constraints, and what we'd be building. No slide deck, no sales script — just a technical conversation. If we're a fit, we move forward. If not, I'll point you to someone who is.
Get in touch