ecommerce

Reetle: from first commit to open pre-orders in fourteen weeks, with a storefront, a checkout and an owned CRM

Fourteen weeks to build the commercial infrastructure for a hardware product: an Angular SSR storefront, a Stripe checkout that authorises at pre-order, an owned CRM, and an automated link to the warehouse. Including the launch state that decoupled software from certification, the Norway that YAML turned into a boolean, and the three bugs we only caught in production.

Contents

Context

Reetle makes a device that snaps magnetically onto the back of your phone: a 3.97-inch E-Ink screen with its own battery that starts recording on a single tap, then transcribes, summarises, and pulls the action items out of what was said. The product was already funded on Kickstarter, with over 90,000 USD raised from more than 700 backers and units shipped to thirteen countries. What was missing was the commercial infrastructure for the European launch.

The brief was the whole thing, not a slice of it: a public storefront, a paying checkout, an owned CRM with segmentation and automation, and the link to the warehouse that actually ships the parcel. First commit: 24 May 2026. On 9 July the site was in production, running in waitlist mode. On 28 August 2026 pre-orders opened, on the same code, with no second launch.

First decision: build it, don't rent it

The obvious route was Shopify Plus with Klaviyo on top. We evaluated it seriously, wrote the comparison down, and rejected it for three reasons we put on paper before any work started.

  • The CRM they asked for was a system, not a report. Identity resolution, RFM segmentation, automation triggers, per-workflow attributed revenue. On the rented stack the "CRM" becomes a read-only window over two vendor systems, and real identity merge means exporting and re-joining data nightly. That costs nearly as much as building it.
  • The launch switch had to be a single value. More on why that mattered so much below.
  • 2,300 EUR a month, recurring, against a one-time build cost. A platform subscription never ends, and migrating off it two years later is a project in its own right, as in the Elisara migration.

We also wrote down what we give up, because a decision without stated costs is not a decision. We lose roughly three weeks of speed against the rented path. We keep more surface to maintain in-house. And we sign the PCI self-assessment ourselves, even though we never see card data.

Certification takes as long as it takes. Software isn't allowed to wait for it.

Any hardware product sold in Europe needs certification, and certification clears when it clears. Nobody can promise it on a fixed date. The classic failure is that the entire commercial launch ends up tied to that calendar, and the software team either idles or ships against a date it does not control.

We cut the dependency. The site has one configuration value with three states: waitlist, pre-order, open sale. It changes from the admin panel in one click, with no redeploy and no code change. Every page, every form and every email knows how to behave differently depending on the current state.

The money side follows the same logic. At pre-order the card is only authorised, not charged. The amount is taken when the unit actually ships. If the communicated window is missed, the authorisation is released and the customer can cancel at any point before the charge.

What we actually built

Three applications sharing one database and one API contract.

  • The public storefront, Angular with server-side rendering and 92 routes prerendered at build time: product page, checkout, legal pages, guides and FAQ.
  • The backend, Spring Boot over Postgres: catalogue, cart, orders, payments, transactional email, the automation engine, and dispatch to the warehouse.
  • The CRM, a separate admin application: orders, customers, campaigns, promo codes with creator commissions, reporting, and the state of every shipment.

The numbers as of publication: 44 database migrations applied in order, 23 architecture decisions written and dated, 11 sprints, and 2,305 automated tests that run on every change (1,007 on the backend, 798 on the storefront, 500 on the CRM). The test count is not a trophy. It is the only reason a configuration value flipped on a Friday evening does not keep us awake.

Payments, shipping, and the country that did not exist

Payments go through Stripe with their hosted card element, so card data never touches our servers. Shipping has two zones, eight dollars each, free above 150 dollars.

We reversed one early decision here, and it is worth saying why. The first version had an explicit list of served countries and refused everywhere else. We inverted the rule: the world zone is now the fallback, so a destination that appears on no list still gets quoted a price. In place of an allow list we put a deny list, checked ahead of zone resolution, holding strictly the countries under comprehensive embargo. The practical effect was that roughly 180 countries became shippable overnight.

A paid order has to reach the warehouse without anyone moving it by hand

The fulfilling warehouse belongs to the client's partner and has its own API. The link between a paid order and a parcel leaving the building is where commerce platforms lose the most money, because it is the one point where a technical failure turns directly into a customer who never receives the product.

We built it as a durable queue in the database, the pattern known as a transactional outbox. When payment is confirmed, the dispatch row is written inside the same transaction. That is all. No vendor call happens inside the transaction, because an HTTP request with a ten-second read timeout would pin a pool connection exactly when the system is under load. A separate process drains the queue in bounded batches, retrying at one, two, four, eight and sixteen minutes.

Two rules, both learned from the vendor's documentation and from how logistics APIs behave under load:

  • We never blindly re-create. A 500 on create can mean the order was created anyway. Every retry queries for the order's existence first, then decides. Our own order number is the idempotency key, so adopting an already-created order is safe.
  • Permanent errors do not burn retries. A product code with no entry in the mapping, or an incomplete address, does not get better by repeating. The row is parked immediately with an alert to an operator. A retry strategy that also repeats what cannot possibly succeed just delays the moment somebody looks at the problem.

The most instructive part was one box on the vendor's form: the recipient's province, mandatory for customs paperwork, which our checkout never collected. We built a component that derives it: in the United States from the first three digits of the ZIP code, in Canada from the leading letter of the postal code, in Australia from the postcode ranges. Everywhere else gets the city. And when nothing resolves, the system does not invent a value. It parks the order in error and calls a human, because a guessed value on customs paperwork costs more than an order delayed by two hours.

Three bugs worth telling

We write these down because the useful part of a case study is not the feature list, it is what broke and why.

One: a prerendered page poisoned the whole application. The workers that generate pages at build time receive a stripped environment, so they could not reach the API. The request failed, the failure was memoised as an error state, and that state was baked into all 92 prerendered pages. In the browser, after hydration, the catalogue read the poisoned state, and the result-sharing mechanism pinned it for the whole session with no retry path. Checkout could not find the product and bounced the user back to the landing page. It only reproduced if you reached the product by clicking through from the landing page, because loading checkout directly touched no prerendered page and worked perfectly. Two fixes, both general: a prerendered state is trusted only when it loaded successfully, and a failure is never memoised.

Two: the mobile menu was imprisoned inside a visual effect. The header had a backdrop filter on it. An element with backdrop-filter becomes the containing block for descendants with position: fixed, and the drawer lived inside the header. So the drawer, which asked for the full viewport, resolved against the header's 64-pixel box. Measured on a real phone: the panel was 64 pixels tall, the link list computed to zero, and page content painted straight through the menu. The fix was moving the drawer out of the header, as a sibling.

Three: the headline never actually shrank on phones. The sizing formula had a 48-pixel floor, and the viewport-proportional term only passed that floor above 600 pixels of width. Result: every phone got exactly the same 48-pixel headline, which wrapped to three lines and pushed the text block up into the region where the scrim over the video was nearly transparent. White text on bright footage. On top of that, those same phones were served a 1093 KB video file. The media attribute on <source> is only honoured inside a <picture>; inside a <video> the browser simply takes the first source, so art direction written in markup alone does nothing. We moved the selection into code, with a 368 KB variant for small screens.

What we cut before launch

The storefront was built in six languages, all taken to full key parity, with translations in the database and not only in the interface. Two weeks before pre-orders opened, the client asked for an English-only launch and dollar-only settlement.

We did it, and we wrote the decision down. Retired locale routes now redirect permanently to their English equivalents, the language switcher and the hreflang signals collapsed to one language, and the translations stayed in the codebase, dormant, ready to turn back on once native review is done. The work was not lost, but it was done earlier than it needed to be. The lesson we carry forward: internationalisation belongs in the architecture from day one, gets switched on at the end, and languages light up one at a time, when there is somebody to proofread them.

Where things stand

On 28 August 2026 ereetle.com opened pre-orders for the Reetle SmartInk I at 149 USD, charged on dispatch. The storefront, the admin panel and the warehouse link have been running on the same infrastructure since July.

Next up: switching the warehouse integration live in production, native review of the translations before the other languages reopen, and the user manual on the product page. The reporting stays the same, including in the weeks when something goes down. Shipping software is 40% of the job; the other 60% is keeping it running cleanly for years (A.06).

Frequently asked questions

How long does it take to build an ecommerce platform from scratch?

For Reetle it was just under seven weeks from first commit to a site in production, and fourteen to open pre-orders. The gap between those two dates is not slower code, it is the rest of the job: payments tested end to end, the CRM, the warehouse link, legal content and translations. A realistic range for a custom-built store with its own admin panel is two to four months, depending on how many external integrations are in scope.

Why build a custom store when an off-the-shelf platform is faster?

Because the difference shows up in what you are allowed to do with your data, not in how fast the first product goes live. A rented platform is the right call when you sell a standard catalogue and need speed. It gets expensive when you need real customer identity resolution, your own automation, or a launch state you control yourself. At Reetle the CRM requirement, with segmentation and revenue attribution, was exactly why the rented stack fell short, and a 2,300 EUR monthly subscription never ends.

How do you sell a hardware product before certification clears?

With pre-orders and a card authorisation, not a charge. The card is authorised when the order is placed and charged only when the unit ships; if the communicated window is missed, the authorisation is released and the customer can cancel. The technical half is giving the site an explicit launch state, stored in the database, that simultaneously changes what the pages show, what checkout accepts and what the emails say. That is how certification and the software launch stop blocking each other.