All work

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.lt

In 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 Garrdu home page on a phone

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 Garrdu product list on a phone

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.

The Garrdu home page with the cart open, on a phone

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.

Garrdu survey step — selecting the dog's parameters
Survey step · server-side progress saving

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.

LP Express parcel-locker picker modal with city grouping
LP Express parcel-locker picker · LT / LV / EE · fault-tolerant search

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.

The Garrdu checkout step on a phone

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.

Garrdu cart with a one-off purchase / subscription toggle
Cart · one-off purchase ↔ subscription · one flow

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.

LP Express parcel-locker list with city grouping
Parcel-locker picker modal · LT / LV / EE

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
Email
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

ComparedShopify + pluginsGarrdu
Monthly SaaS subscription totalfrom €600€0 (every piece built custom)
Payment channels at checkout1–23 (card · Apple/Google Pay · bank link via Makecommerce)
Ad tracking after Safari ITPPartial (browser only)Full (server-side + deduplication)
Subscription personalizationA single template for everyoneSet from the survey (portion + frequency)
Abandoned-cart recoveryA plugin with a per-profile feeA custom in-house solution, no monthly fee
First paint on mobileA templated JS shellServer-side SSR
Lithuanian SEO visibilityA plugin plus delayed content after hydrationServer-side SSR with Lithuanian meta tags from the first byte
Ownership of customer dataBelongs to the platform100% 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