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
Situation
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.
Solution
How we got there
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.
Result
What changed
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.
Narrative
How it was built
01 / START
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.
02 / POSITION
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.
03 / SURVEY
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.
Survey step · server-side progress saving
04 / TRACKING
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.
05 / EMAIL
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.
06 / RECOVERY
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.
06b / DELIVERY
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.
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.
08 / INSIDE
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.
09 / INFRA
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.
Expertise
Payments — three channels, one flow
[ Payments + Subscriptions ]
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.
Cart
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.
[ Cart · One-time vs Subscription ]
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.
Shipping
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
✓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.
Parcel-locker picker modal · LT / LV / EE
Interface
On mobile
Home · heroProduct listCart modalCheckout · mobile
Tech stack
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
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*.
Challenges
What had to be solved
01
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.
02
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.
03
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.
04
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.
05
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.
06
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.
07
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.
08
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.
09
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.
Advantages
Where it beats the standard
01All 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.
02Attribution 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.
03A 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.
04Three 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.
05An 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.
06An 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.
07Speed 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.
08GDPR built into the foundation
Consent events, an audit log, an unsubscribe chain — no extra plugins, no legal-risk patch job.
09Brand 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.
10A predictable bill
A fixed monthly server cost, with no percentage of sales and no surprises in January.
Comparison
Garrdu vs. the standard
Metric
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
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.