In-house e-commerce · subscriptions · D2C
Garrdu
A top-tier, current-generation e-commerce build. Comparable Shopify projects run from €600 a month.
- Year
- 2023–2025
- Role
- Build, design, architecture, upkeep
- Duration
- Iterative (12+ months)
- Industry
- Premium pet food · D2C subscription
Hover over a preview to see the whole page.
Visit garrdu.ltIn short
- 3 payment channels (card with 3DS, Apple Pay / Google Pay, Baltic bank links via Makecommerce) in one checkout flow
- A survey built on the dog's weight, age and activity sets the portion size and delivery schedule
- Server-side conversion tracking: Meta and Google still get the purchase signal after Safari ITP
- An in-house email and abandoned-cart engine instead of a Klaviyo profile fee
- A predictable monthly server bill instead of Shopify's cut plus a stack of SaaS subscriptions
The problem
A premium dog-food brand needed a direct sales channel in Lithuania. Every dog needs its own portion size, feeding frequency and delivery schedule computed from age, weight, body type and activity level. A typical Shopify plugin doesn't do that; a WooCommerce workaround would mean stacking six or seven SaaS subscriptions that don't talk to each other and get pricier as the business grows.

The solution
We started from an empty file. One database, one source of truth for the order. An Express-based backend with Prisma. A Vite + React 18 front-end with hand-rolled SSR* and react-helmet-async*. Three payment channels behind a single state machine*. BullMQ* queues for the abandoned-cart and email chains. Conversion tracking from the server, not just the browser. Audit log, consents and admin built in-house.

The result
The client got an online store that functions as the backbone of a serious D2C business: three payment methods in one flow, a survey-built subscription, an in-house email engine, and server-side conversion tracking that survives Safari ITP and ad blockers. No SaaS subscription fee, no percentage of sales.

How it was built
Starting point
Garrdu arrived with a brief that templates don't answer: sell premium dog food as a subscription, where every dog's portion size, feeding frequency and cart are set by age, weight, body type and activity — and all of it has to feel equally smooth on a phone at 4am and in an office on Monday morning. You don't get there with Shopify plugins. So we didn't start by picking a theme — we started from an empty file.
Why not Shopify
A growing Lithuanian store eventually lands in the same place: a platform fee, a subscription plugin, an email tool, an abandoned-cart tool, an upsell tool, review and loyalty plugins, a survey builder. Each one runs €30–300 a month, each one holds its own island of data, and each one breaks when its neighbour updates. Garrdu skipped that stage entirely: all the logic lives in one system — one database, one source of truth for the order, the customer and the subscription.
A survey that does the salesperson's job
Garrdu's nine-step survey turns what an owner knows about their dog into a specific product in the cart and a subscription delivery schedule.
In the first five steps the owner just clicks answers, typing nothing: the dog's age group (puppy, adult, senior — from 2 months to 12+ years), activity level, digestive quirks, food fussiness and allergy status. A sixth step appears only when the previous answer suggests or confirms allergies — the owner flags the specific proteins (chicken, turkey, beef, pork or venison) the dog can't tolerate. Step seven takes weight in kilograms, step eight the dog's name, step nine an email address. The last two are only asked once the person is already committed.
From the answers, deterministic logic* — no LLM* guessing involved — picks one of four proteins by a strict priority order: allergies first, then digestion, then activity. A confirmed allergy always rules out that protein; with no allergies, frequent digestive issues lead to venison, high activity with stable digestion to beef, and a dog with no particular issues gets turkey as the everyday default. Once the protein is set, the daily portion in grams is computed by linear interpolation* against the product's own weight table (for example, a 10 kg dog gets roughly 100 g a day), which then determines how many packs are needed per month and how often they ship.
The survey's progress is stored on the server, so someone can start on a work computer and finish on their phone that evening — sessions are matched by email even when the two browsers carry different cookies. After an accidental close, the same question the person left off on reopens. Once the survey is done, the product lands in the cart with its portion and delivery frequency already computed.

Attribution that survives Safari ITP
Over the past couple of years, browsers have cut off a large share of the data Shopify-style stores rely on. At first glance ads look like they're converting worse, when in reality they simply stopped being visible. Garrdu's conversion tracking doesn't run in the browser alone: the core event chain — purchase, subscription start, survey completion — is also sent from the server via CAPI*, each with a unique event ID*, geographic context and deduplication*, so Meta and Google count the same event once. A quality monitor sits alongside it, catching a broken event before the ad budget starts burning on bad data.
An in-house engine instead of Klaviyo
Confirmation emails, abandoned-cart reminders, campaigns, unsubscribe links, customer segments — all built in-house. A queue, a scheduler, open- and click-tracking, and a one-click unsubscribe* flow that meets GDPR* requirements without extra plugins. As the customer base grows, the bill rises far more slowly than a Klaviyo per-profile fee — it scales with server resources, not with customer count.
The abandoned-cart chain
The abandoned-cart chain sends three emails: a first reminder after an hour, a second the next day, a third with a product summary and a discount — but only when the logic shows the person is still hesitating. If the buyer completes the order or cancels the subscription in the meantime, the chain stops. Once the recovery token* expires, its path closes without a final email.
LP Express parcel lockers — built in-house
A Lithuanian buyer expects to pick up their parcel from the nearest locker. A ready-made LP Express plugin simply doesn't exist on the market — anyone who has needed one in a Lithuanian store has had to build it themselves. For Garrdu we built the parcel-locker picker layer from scratch.
We start from the whole Baltic region: 1,174 terminals in a single search (447 in Lithuania, 539 in Latvia, 188 in Estonia). All of it sits in one CSV* data source with coordinates, cities and postal codes, refreshed directly from LP Express. The list is grouped by city, cities are sorted alphabetically, and each city's terminals are sorted by address. Even when a single city holds twenty locations, they stay in a sensible order.
Search isn't a plain "starts with…" filter. Underneath sits a Fuse.js* engine with four weighted fields: city, address, terminal name and postal code. The threshold sits between 0.4 and 0.5 — loose enough that typing "Vinius" finds Vilnius locations and "Bazanavič" finds terminals on Basanavičiaus street, but tight enough that results don't fill up with random noise.
The interface switches by device: on phones and touch tablets it opens a full-screen modal* with large tap targets, on desktop a compact dropdown sits alongside the other form fields. The chosen terminal's details (name, street, city, postal code) flow into the order, the LP Express label and the confirmation email. For a subscription, the terminal is saved for the next shipment — a repeat order never has to pick it again.

Speed
The first thing a visitor sees is assembled server-side via SSR*, not loaded in afterward via JavaScript. That's why Google's crawler sees Garrdu's real content immediately, not an empty shell. Images reserve their space in advance with LQIP hints, so nothing jumps around once a visitor starts tapping. The hero zone*'s gradient is drawn in real time via WebGL* — not loaded as a heavy background image. It's invisible work, but it's exactly why Core Web Vitals* show green across the board in a Lighthouse* audit.
Admin and the GDPR paper trail
Behind the scenes sits an internal admin layer: orders, coupons, campaigns, customers, subscriptions and an audit log where every internal action gets its own line. Customer consents are stored as separate records — when, for what, and under which version of the privacy policy. If a request from the State Data Protection Inspectorate ever comes in, the answer takes minutes, not weeks.
Infrastructure and a predictable bill
Garrdu runs on servers we manage: containers via Docker, a reverse-proxy* in front, health checks*, and a deploy* several times a week. No Shopify percentage cut, no pressure to upgrade to a Plus plan as the business grows, no plugin price hikes in January. The monthly bill doesn't depend on whether you sell 50 packages or 5,000 — only on the server-resource ceiling, where capacity needs bumping up.
Payments — three channels, one flow
In Lithuania, one buyer pays by card, another confirms with a fingerprint via Apple or Google Pay, a third picks a bank link from their own bank's list. In most stores these are three separate systems that don't talk to each other. In Garrdu's system they're routed through a single state machine, so however the money travels across the three paths, the order, the customer and the subscription end up in the same place.

Card payment
Stripe · 3DS · AVS · vault
- 3D-Secure flow with a separate retry* window
- AVS*-decline retry* without re-entering the card
- Deduplication of Stripe webhook* messages via event ID*
- A vaulted token for repeat purchases
- Stripe Subscription lifecycle tied to the subscription model
Apple Pay / Google Pay
Phone wallet · fingerprint confirmation
- Offered automatically when the browser and device support it
- The buyer never enters a card — confirms with Face/Touch ID or a PIN
- Works for both a one-off purchase and starting a subscription
- A shorter checkout flow — fewer carts abandoned at the last step
Makecommerce
Baltic bank links · LT / LV / EE
- The buyer picks their own bank from a list and confirms payment in online banking
- The callback* cycle is synced with Stripe's states — the same success branch
- The bank list is filtered by the buyer's country (LT / LV / EE)
- Expiry windows and a short recovery window are handled by separate logic
Once a payment succeeds through any channel, the order, subscription and customer land in the same place. In many online stores one of the channels is bolted on the side — in Garrdu's system all three are woven into the main flow.
A cart with two modes
In the same session a buyer can choose a one-off purchase or a subscription — the same product, in the same cart, through the same payment method. In many online stores a subscription lives on a separate branch with its own interface, usually via an external tool (ReCharge, Bold, and the like). In Garrdu's system, one flow launches two separate lifecycles from the same cart.

One-off purchase
One card · one charge · done
- A single Stripe PaymentIntent* or Makecommerce request
- 3DS, AVS or the Apple/Google Pay layer kicks in when needed
- The order, receipt and LP Express label form immediately after success
- Card data and the webhook window don't persist
- Fits a first purchase, a gift, a one-off test
Subscription
Vaulted token · recurring charges · managed from admin
- A Stripe Subscription* off the same cart (card or Apple/Google Pay)
- The payment method is stored in Stripe's vault*, not with us
- Delivery frequency (weekly or monthly) — set by the survey, or changed manually
- Webhooks handle renewal, pause, a failed charge and cancellation
- The customer sees one invoice, one shipment, one frequency control
Same product, same cart, same checkout window — but two different journeys begin right after it. In many online stores a subscription is kept separate and never connected to one-off order logic; Garrdu's system crosses that line inside a single object.
LP Express integration from scratch
Partner: LP Express – Lithuanian Post · terminals LT / LV / EE
The delivery step is where Lithuanian online stores often break down: either only door delivery is on offer, or the parcel locker has to be picked from a long, unsorted list. For Garrdu we built a parcel-locker picker layer with no ready-made plugin to lean on — none exists on the market.
- Every LT, LV and EE terminal in a single search
- Grouped by city with clearly visible city headers
- Fault-tolerant search (typing "Vinius" finds "Vilnius pst.")
- A full-screen modal* on mobile; a compact dropdown on desktop
- Courier delivery offered as an alternative in the same step
- Terminal details carried through to the order, the label and the confirmation email
- A repeat order from a subscription: the terminal is remembered from last time
A detail Lithuanians value more than any survey shows: a parcel locker you can find on your phone in seconds, instead of a page you have to pull up on a desktop.

Architecture
A hand-assembled SSR* setup on Vite + React 18. An Express backend with Prisma over PostgreSQL (around 30 models). Redis + BullMQ* for four queues (abandoned-cart recovery, transactional email, marketing campaigns, the email processor). An in-house auth layer (JWT*, bcrypt*, admin sessions, audit events, service API keys). Deployed via Docker + Coolify* + nginx + PM2*.
- Front-end
- Vite · React 18 · hand-rolled SSR · react-helmet-async
- Back-end
- Node.js · Express · TypeScript · Zod
- DB
- PostgreSQL · Prisma (~30 models)
- Queues
- Redis · BullMQ (4 workers)
- Payments
- Stripe (card + Apple/Google Pay) · Makecommerce (Baltic banks)
- Tracking
- PostHog (proxied) · Meta CAPI · GeoIP2 · event-ID dedup
- In-house engine · scheduler · campaign chains
- Auth / security
- JWT · bcrypt · audit log · rate limiting (2 layers)
- GDPR
- Consent events · privacy-policy version records · unsubscribe tokens
- Ops
- Docker · Coolify · nginx · PM2 · Retool dashboards
- i18n
- i18next + http-middleware (LT / EN)
- Speed
- Server-side SSR · LQIP hints · WebGL gradient
- Vite + React 18
- Hand-rolled SSR
- Express + Prisma
- PostgreSQL
- Redis + BullMQ
- Stripe (card + Apple/Google Pay)
- Makecommerce (Baltic banks)
- PostHog proxy
- Meta CAPI
- MaxMind GeoIP2
- Docker · Coolify · nginx · PM2
- i18next (LT/EN)
Automation and background processes
Four BullMQ workers* handle what the customer never sees: abandoned-cart recovery with recovery tokens, transactional email, marketing campaigns and the email processor. The survey logic is deterministic, not an LLM* guess — it computes the portion and the subscription delivery frequency from the answers. The tracking chain between the browser's Meta Pixel* and the server-side CAPI* deduplicates events via a shared event ID*.
What had to be solved
One source of truth between Stripe and Makecommerce
Two payment providers whose event semantics don't line up. Stripe sends webhooks following a purchase's lifecycle; Makecommerce follows the outcome of short-lived bank redirects. We needed a chain that could fold both sides' outcomes into a single order-and-subscription record with no duplicates.
Baltic bank links alongside a global channel
Makecommerce's callback behaves differently from a Stripe webhook: a short expiry window, different retry logic, different fields. Weaving this channel into the same state machine as Stripe took longer than the visual design did.
Event deduplication across the whole purchase path
The browser's Meta Pixel and the server-side CAPI need to send the same purchase event, not two. If the event ID, timing or context diverges, Meta optimizes against a distorted picture. We wrote the deduplication as a standalone library with its own tests, so it can change without re-reviewing the whole funnel.
A timing map for consent
Pixel and server-side events behave differently depending on the consent banner's state. We had to define precisely which event fires before consent (and in what form), and which one waits for confirmation — otherwise you either breach GDPR or lose a conversion.
Abandoned-cart edge cases
A recovery token expires. A customer finishes the purchase between two reminders. Someone cancels a subscription mid-chain. We wrote an explicit exit for every scenario, so a recipient never gets an email about something that's already gone.
Survey session handoff across devices
Started on a computer, finished on a phone that evening; different cookies, no shared session. Without a server-side key tied to the email address, that would have been data loss, not a feature.
A mix of payment steps under one order
3DS confirmation, an AVS retry, an Apple or Google Pay prompt, Makecommerce's back-and-forth dance with the bank — each has to fit its own provider's rules. A wrong step ends in a decline you see not in the buyer's dialog, but in telemetry.
Server-side i18n wiring
Lithuanian meta tags have to land in the server-generated HTML before the first paint, and later hydration can't overwrite them. A small detail, but it's why Google reads Garrdu as a Lithuanian brand rather than an echo of English keywords.
A deploy chain with no surprises
Separate Dockerfile variants for production and staging, different nginx configs, one process manager. All of them have to behave identically on both sides — otherwise an error surfaces in front of a buyer that never showed up in any staging run.
Where it beats the standard
All the logic under one roof
Instead of six or seven SaaS subscriptions talking to each other over webhooks, there's one database and one backend. Every feature sees the full context.
Attribution after Safari ITP
Server-side tracking means Meta and Google still get the purchase signal even when the browser stays quiet. Optimization doesn't run blind.
A survey instead of a human consultation
Portion and delivery frequency are set from the dog's weight, age, activity and allergy status. A Shopify plugin doesn't do this work.
Three payment methods, one flow
Card with 3DS, Apple/Google Pay, and Baltic bank links via Makecommerce — all with the same consequences for the order. Fewer carts abandoned at the last step.
An abandoned-cart recovery chain
Not a single reminder, but a chain that stops itself once an order completes or a subscription is cancelled. Replaces an expensive external tool.
An in-house email engine
Functionally comparable to Klaviyo, without a fee for every active profile. The marketing bill grows far more slowly than the customer count.
Speed from day one
Server-first paint, optimized images, space reserved in advance. Core Web Vitals aren't a final checkbox here — they're a starting condition.
GDPR built into the foundation
Consent events, an audit log, an unsubscribe chain — no extra plugins, no legal-risk patch job.
Brand character
An animated checkout flow, an in-house hero gradient, custom modals. It's visible to the customer that this isn't another Shopify template store.
A predictable bill
A fixed monthly server cost, with no percentage of sales and no surprises in January.
Garrdu and the standard setup
| Compared | Shopify + plugins | Garrdu |
|---|---|---|
| Monthly SaaS subscription total | from €600 | €0 (every piece built custom) |
| Payment channels at checkout | 1–2 | 3 (card · Apple/Google Pay · bank link via Makecommerce) |
| Ad tracking after Safari ITP | Partial (browser only) | Full (server-side + deduplication) |
| Subscription personalization | A single template for everyone | Set from the survey (portion + frequency) |
| Abandoned-cart recovery | A plugin with a per-profile fee | A custom in-house solution, no monthly fee |
| First paint on mobile | A templated JS shell | Server-side SSR |
| Lithuanian SEO visibility | A plugin plus delayed content after hydration | Server-side SSR with Lithuanian meta tags from the first byte |
| Ownership of customer data | Belongs to the platform | 100% belongs to you |
See it live: Garrdu store
What changed
- Payment channels
- +1–2 channels
- 1–2 → 3 in one flow
- Subscription personalization
- Individual to each dog
- Templated → Set by survey
- Ad visibility after Safari ITP
- Attribution no longer runs blind
- Partial → Full
- Monthly SaaS subscriptions
- Every piece built custom
- from €600 → €0
- First paint
- Faster
- JS shell → Server-side SSR
- GDPR audit
- Minutes, not weeks
- A bundle of plugins → Recorded in the DB
- Lithuanian SEO visibility
- Full indexing from the first visit
- Plugin plus delayed content → SSR plus Lithuanian meta tags from the first byte
- Survey step logic
- Portion and protein set deterministically
- One flow for everyone → Branches by allergy and activity
The specific implementation recipes — the event-ID schema, the Stripe vault-and-state chain, the survey's portion formula, the abandoned-cart timing map, the deploy sequence — aren't shared publicly. Presented separately to a serious client.
Need a website like this?
Tell us what you have in mind. We read every inquiry ourselves.
Discuss a project
