Skip to content
13Case studySW · 13 of 10

Made-to-order commerce for a market that needs its own stack.

Generic ecommerce stacks couldn't model the financial and logistical primitives this market requires. Monobank split-payment, Nova Post fulfilment, per-product multi-currency — built from scratch for a Ukrainian maker brand shipping to 7 locales.

ClientConfidential
Year2026
Duration1 yrs
StackTypeScript · Next.js · React · Postgres · Vercel · AWS
Hero image for maker-brand-commerce-platformFIG 01 · HERO

The maker's constraint.

Handmade goods from Ukraine, shipping to seven countries. The financial and logistical infrastructure the market runs on — Monobank for payments, Nova Post for fulfilment, the National Bank of Ukraine's daily FX rates for pricing — has no counterpart in a generic European ecommerce stack.

The payment model alone rules out Stripe. Monobank's split-payment model collects a deposit at order confirmation and fires the balance charge when the order reaches 'ready to ship.' That's two payment legs, timed to production state, with ECDSA webhook signatures at every transition. Stripe has no primitive for this. You'd have to fake it with two unrelated charges and reconcile state yourself — and any reconciliation gap is a fraud surface.

The logistics model rules out Shippo. Nova Post, Ukraine's dominant carrier, exposes a directory of cities, divisions, and classifiers that must be synced and queried locally. Waybills are generated programmatically at ship time and the resulting label PDF needs to live somewhere retrievable. Nothing in the generic-carrier abstraction world maps to this cleanly.

The pricing model rules out per-region currency settings. The maker sells some products globally in EUR, others only in UAH because the supply chain is local. A few products carry explicit UAH or USD overrides that differ from the FX-converted base. Per-region rules can't express 'this specific product has its own UAH price regardless of the buyer's locale.' Per-product overrides can.

A Stripe + Shippo + WooCommerce setup cannot model 'deposit-now in UAH, balance-on-ready in EUR, fulfilled by Nova Post' without custom code at every seam. The custom build wasn't a preference — it was a requirement of the market.

What we built.

One platform: a 7-locale customer-facing storefront, a production lifecycle (draft → in-progress → ready → shipped), and an ops dashboard the founder runs alone.

The payment layer handles Monobank's full split-payment workflow. Deposit collected at order confirmation. Balance charge triggered when the order state transitions to 'ready to ship.' Every webhook is verified against a cached Monobank PEM public key using ECDSA SHA256 with rotation. Payment legs are records in the database, linked to the order state — not reconciled post-hoc.

The pricing layer reads per-product currency overrides before falling back to FX-converted base. Each product row has a EUR base price and optional UAH and USD override columns. When an override is present, the storefront uses it. When it's absent, the National Bank of Ukraine's daily FX rate converts the EUR base. The NBU rate syncs via cron every day.

The logistics layer runs on a synced Nova Post directory. Cities, divisions, and classifiers are paginated in from the Nova Post API and stored locally — checkout address selection queries the local copy, not the live API. At ship time, the platform generates a waybill through Nova Post's API and stores the resulting label PDF as a presigned object in Cloudflare R2.

The production lifecycle is the scaffolding that ties the payment legs and shipping together. The order state machine — five states, an event log, a progress-photo rail — exists so the deposit-to-balance transition has a defined trigger and the waybill generation has a defined moment. The state machine isn't the product. It's the load-bearing frame for the financial and logistical flows.

F · 01Monobank split-payment
Deposit collected at order confirmation; balance fires on 'ready to ship.' Payment legs are database records linked to order state. ECDSA SHA256 webhook verification with PEM key rotation.
F · 02Per-product multi-currency
EUR base price with optional UAH and USD override columns per product. Pricing resolver reads the override before falling back to the NBU daily FX rate. Maker-level control, not region-level.
F · 03Nova Post integration
City and division directory synced locally via scheduled job. Waybills generated at ship time through Nova Post's API. Label PDFs stored as presigned objects in Cloudflare R2.
F · 04Production lifecycle
Five-state order machine (draft → in-progress → ready → shipped → cancelled) with event log and progress-photo rail. Scaffolding for the payment legs and waybill generation — not the central object.
F · 057-locale storefront
Customer-facing store in seven languages. Per-product currency overrides and NBU FX fallback mean each locale sees contextually correct pricing without per-region configuration.
F · 06Purpose-built ops dashboard
Production kanban, 30-day EUR revenue sparkline, integration health pills (Monobank / Nova Post / R2 / email), cron freshness display, and an attention strip. One screen for the full operation.

The fintech engineering choices.

ECDSA webhook verification. Monobank signs payment webhooks with a private key; the receiver must verify against a cached public key, rotate the cache when Monobank publishes a new one, and reject any webhook that doesn't pass the signature check. Toy auth would be a checked header value. Real auth is ECDSA SHA256 against a PEM key with rotation.

Payment legs at the data layer. The split-payment model isn't a payment-gateway config. It's a data model: an order has one or more payment legs, each with an amount, a currency, a status, and a trigger. The deposit leg fires at order confirmation. The balance leg fires at state transition. The event log carries the audit trail. Reconciling two unrelated API charges after the fact would mean the source of truth is Monobank's dashboard, not your own database. We refused that trade.

Per-product currency overrides. The pricing table has three optional price columns beyond the EUR base: UAH, USD, and a 'price on request' flag. The pricing resolver reads them in priority order: explicit override → FX-converted base → request. The maker controls pricing at the product level, not the region level.

The local-market specificity is the moat. A competitor can copy the storefront design. They can't run the same payment model without rewriting the payment layer for Monobank's specific API.

Nova Post directory sync. Calling the Nova Post live API on every checkout address input would couple the purchase flow to an external dependency with no SLA the platform controls. The directory sync runs as a scheduled job: pages of cities, divisions, and classifiers written locally, checkout queries the local copy. The only Nova Post live call in the critical purchase path is the waybill generation at ship time — and that happens after the order is already paid.

The outcome.

The founder runs the entire operation from one screen. Production kanban by order state, a 30-day revenue sparkline in EUR, integration health pills for Monobank, Nova Post, R2, and email, cron freshness display, and an attention strip that surfaces anything needing action. About 1–2K lines of dashboard component — purpose-built, not generic.

145 commits in 3 weeks. 659 test files covering the Monobank ECDSA path, Nova Post directory reconciliation, cart bundle semantics, currency snapshotting, and payment-leg state transitions. Sentry traces at 5% in production with a PII scrubber. Axiom for structured cron heartbeat events. Production-grade observability wasn't retrofitted — it was part of the first sprint.

PULL QUOTE / 04
The stack isn't custom because we wanted to write custom code. It's custom because the market has no off-the-shelf rails.
FounderMaker-brand commerce
Outcome
Build time
3 weeks
Locales
7
Test files
659
Commits
145
NEXTCase study 01SW · 01 of 10
Real estate2022 — 2024

An AR property-viewing app for buyers who can't be in the room.