Hiring? Start here →

Same store, second storefront

Open the Liquid theme version

PORTVA also runs as a hand-built Liquid theme on the same Shopify store. Shopify development stores always show a password page first — paste the password once and your browser remembers it.

Store passwordpaucai
PORTVA

Your cart

Your cart is empty.

Continue shopping

Case studies

Seven problems, and why they were solved this way

The first three come from a live DTC brand store, anonymised. The rest were built on PORTVA. Each one covers what made the problem hard, which options were on the table, and what the chosen one costs.

01 · Live store · Full-stack

Telling shoppers “fits” only when the data says so

Every page now reads one compatibility result with three states, so a gap in the data no longer shows up as a promise.

This store sells parts that only work with certain models, so every page has to answer one question: will this work with what I own? A blank field used to mean “fits all”, and each page type worked out the answer on its own.

Full reasoning: constraints, options, verification and cost

What made it hard

  • Much of the compatibility data is incomplete, and “unknown” was not a value it could hold.
  • The answer is needed on model pages, product pages, a sizing guide and a bundle builder.
  • One model can come in several configurations.
  • Shoppers who haven't chosen a model still have to be able to buy.

Options

  • Keep “fits / doesn't fit”Not chosen

    Simple, but missing data still reads as a promise, and that becomes returns and complaints.

  • Patch each page separatelyNot chosen

    Fastest for one page, but the same model and product can get different answers across the site.

  • One shared result: fits, conditional or unknownChosen

    Pages display the result and never guess. Unverified never renders as “fits”, and a conditional result asks the shopper one more question.

What shipped

  • A shared compatibility result used across model, product, guide and bundle pages.
  • Conditional results ask one or two follow-up questions, the most decisive first.
  • Single-product purchase is never blocked without a chosen model. The check is offered instead.

How it was checked

  • Each release went through several rounds of pull-request review and preview checks on representative models.
  • Ambiguous data rows went to the operations team instead of being resolved in code.

What it costs

  • Models without reviewed statuses have to stay out of the new behaviour until their data is done.
  • Three states add a loading and “still checking” case to every purchase path.

02 · Live store · Full-stack

Adding a model without touching the theme

A new model is now a data release that operations can publish and roll back, using an internal Shopify app.

The store rolls out landing pages model by model. Doing that by hand would mean a theme change and a developer for every model, and a bad data file would go straight to customers.

Full reasoning: constraints, options, verification and cost

What made it hard

  • The theme is shared with the rest of the live store, so every template change carries release risk.
  • Operations maintains the data in spreadsheets and needs to see what changes before it goes live.
  • The data set is too large to edit safely by hand in the Shopify admin.

Options

  • One template per modelNot chosen

    Quick for the first few, but the theme grows with every model and each one needs a release.

  • Direct edits in the Shopify adminNot chosen

    No build needed, but there is no diff, no review and no way back after a mistake.

  • Data-driven pages plus a versioned publishing appChosen

    Metaobjects give each model a standard URL. Data is uploaded, validated, reviewed as a diff and published as a new version.

What shipped

  • Model pages rendered from data, with crawlable links and matching structured breadcrumbs.
  • A Shopify app on React Router, Node and Prisma: upload, validate, review the diff, publish, roll back.
  • Large imports run in a background worker. The storefront reads whichever version is current.

How it was checked

  • The app is in production use for the store's compatibility library.
  • Rolling back is a routine step: the storefront is pointed at the previous published version.

What it costs

  • The app is another system to host and maintain.
  • The theme and the app share a data contract, so changing its shape means coordinating both.

03 · Live store · Tracking

Making sure a campaign can be measured afterwards

New pages and promotions now ship with a defined GA4 funnel and order attribution, after one campaign couldn't be evaluated.

A promotional feature went live with no events and shared discount codes. Afterwards nobody could compare shoppers who used it with those who didn't, or tie orders back to it with confidence.

Full reasoning: constraints, options, verification and cost

What made it hard

  • Team preview links and launch-day checks were being counted as real visits.
  • A crawler could dominate a day's traffic and distort every rate.
  • GA4 reserves some parameter names for its own ecommerce data.
  • No personal data may go into the events.

Options

  • Rely on default GA4 and Shopify reportsNot chosen

    No work needed, but they can't isolate a feature or a page's funnel.

  • Attribute by discount codeNot chosen

    Easy to read, but shared codes spread beyond the feature and overstate or blur the result.

  • Plan the funnel before buildingChosen

    Exposure, click, participation, add to cart and purchase are defined up front, with a campaign marker carried on the cart for attribution.

What shipped

  • GA4 funnel events across the model pages and site-wide model selector.
  • Order attribution through cart attributes, and the shopper's model passed into abandoned-cart email.
  • A test flag that keeps team visits out of reports.

How it was checked

  • Events were reviewed against real reports, and reporting starts the day after launch to skip test traffic.
  • Known crawler traffic is excluded before any rate is read.

What it costs

  • Every new feature carries event design and QA time.
  • Reports only become useful after enough clean days have passed.

04 · PORTVA · Full-stack

A variant that must not sell in the wrong country

A wrong-plug order is stopped at checkout, whichever storefront or cart link it came from.

Portva 140 comes with a US or an EU plug. A US plug is useless in Germany, so that variant must not be sellable there, yet both plugs belong to one product, one page and one option picker.

See the US plug disabled in Germany →

Full reasoning: constraints, options, verification and cost

What made it hard

  • Shopify Markets decides availability per product, not per variant.
  • There are two storefronts, Next.js and Liquid, and a cart can be built without either interface.
  • German translations rename the option (Plug → Stecker), so the option name can't be the key.
  • Functions see the variant title, such as “Graphite / US”, not its selected options.

Options

  • One product per plug, published per marketNot chosen

    Availability would be native, but the plug stops being an option. Stock, URLs and analytics split by market, and every new colour means more products.

  • Hide the wrong plug in the interface onlyNot chosen

    Quick, and enough for most shoppers, but a shared cart link or the other storefront goes straight past it.

  • Storefront rule plus a server-side checkChosen

    Both storefronts mark the plug as not sold here and never pick it by default. A Cart Validation Function enforces the same rule inside Shopify.

What shipped

  • Each option value has three states: available, sold out, or not sold in this market.
  • The charger finder and quick add choose a variant that can be sold in the current market.
  • At checkout the shipping address overrides the browsing market, and the error appears in English or German.

How it was checked

  • Four fixtures run through the compiled Function: EU plug to Germany passes, US plug to Germany is blocked, a US-market cart shipping to Germany is blocked, and countries without a rule are left alone.
  • One bug surfaced on the way: the rule broke once German translations renamed the option. It now matches the plug code.

What it costs

  • The country-to-plug table lives in three places: Next.js, Liquid and the Function. A third market means three edits.
  • The Function reads the plug from the variant title, so renaming option values needs a matching change.

05 · PORTVA · Front-end

Real-time 3D that doesn't tax the first load

PageSpeed Insights couldn't finish testing the mobile home page. It now scores 95, and the 3D is still there.

The home page tells its story with a live WebGL model. On a mid-range phone that tied up the main thread for seconds, before a shopper had seen anything to buy.

Read the full performance report →

Full reasoning: constraints, options, verification and cost

What made it hard

  • three.js and its renderer added about 260 KB of JavaScript to the first load.
  • A loop that draws every frame never lets a page go idle on a machine without a GPU, which is how lab tests run.
  • The first paint still had to show the product and match the live model when it arrived.

Options

  • Replace the 3D with a videoNot chosen

    Light and reliable, but it gives up the interaction, which is the point of the section.

  • Keep the 3D and load it once the page is idleNot chosen

    Better first paint, but phones still download a model for visitors who leave without touching the page, and the render loop remains.

  • Poster first, three.js on first interaction, draw only on scrollChosen

    The poster is a render of the same model, so the swap is a cross-fade. The canvas draws a frame only when the scroll timeline moves.

What shipped

  • three.js and the model load on the first scroll, touch or mouse move.
  • Visitors with reduced motion or no WebGL get a static hero with the same content.
  • Photos are staged and pre-sized, and the product page's buy area moved into the prerendered HTML.

How it was checked

  • Mobile PageSpeed on the home page: could not complete before, 95 after (median-LCP run of three).
  • Local Lighthouse, median of three: JavaScript 557 → 295 KB, main-thread work 4,435 → 1,596 ms, blocking time 522 → 55 ms.

What it costs

  • On a phone the live model replaces the poster about a second after the first touch.
  • Any future idle animation has to request its own frames.
  • On the product and collection pages the PageSpeed change is within noise. All of this is lab data, with no real traffic to confirm it.

06 · PORTVA · Full-stack

One honest price per market, from product page to checkout

A German shopper sees the same fixed euro price with VAT included, on either storefront and through to checkout.

German prices were a daily conversion from dollars, which produced figures like €108.95 that moved on their own. German shoppers also have to see prices with VAT included.

See the German price on the live site →

Full reasoning: constraints, options, verification and cost

What made it hard

  • Prices are read in four separate places: the headless site, the Liquid theme, the cart and a checkout extension.
  • Product data has to stay cacheable, so each URL must resolve to exactly one market.
  • A cart belongs to a buyer country, and some of its lines may not be sellable in another one.

Options

  • Keep automatic conversionNot chosen

    No upkeep, but the price changes daily and is never a number a brand would choose.

  • Keep one cart and change its countryNot chosen

    Prices would update, but a US-plug line would travel into a German cart.

  • Market in the URL, its own price list, one cart per marketChosen

    Every request knows its market before it reaches Shopify, Germany's prices are set on purpose, and a cart can never mix markets.

What shipped

  • Germany has its own catalogue and fixed euro prices: Portva 140 €129, Portva 65 €55, Link €21 and €27.
  • Every storefront query runs in the page's country and language. Carts are created for that country and stored one per market.
  • Both storefronts show “inkl. MwSt., zzgl. Versand” based on the market, and the German checkout shows VAT.

How it was checked

  • A German test checkout exposed a real bug: the upsell extension queried Shopify without a market and offered the cable in dollars. It now asks in the buyer's country and shows 21,00 €.
  • The setup script is idempotent, so running it again leaves the catalogue and prices as they are.

What it costs

  • Switching market starts a different cart, so items don't carry over.
  • Fixed prices need someone to review them when costs or exchange rates move.
  • The cart's free-shipping bar repeats the store's threshold of 70 in the front-end, so changing the rule means changing both.

07 · PORTVA · Full stack

Back-in-stock alerts that handle each stock event once

After a restock, Shopify's webhook updated the subscription on the live site within 6 seconds. Forged requests and repeat deliveries have no effect.

Sold-out variants need a way to ask for an alert. Stock changes arrive as Shopify webhooks, which can be retried or arrive concurrently, and a public endpoint can be forged.

See the alert form on a sold-out variant →

Full reasoning: constraints, options, verification and cost

What made it hard

  • The site runs on Vercel and has no database of its own.
  • Shopify expects a quick response, and a failure must still be retryable.
  • This is a concept store, so it shouldn't keep visitors' real addresses for a demo.

Options

  • Remember processed events in memoryNot chosen

    Serverless instances don't share memory, so concurrent deliveries and retries get through.

  • Add a database for this featureNot chosen

    It gives a unique constraint, but adds an account and operations for two small kinds of record.

  • Store subscriptions in metaobjects and use the event id as a unique handleChosen

    Shopify enforces handle uniqueness, so only one of two concurrent deliveries can create it, with no extra infrastructure.

What shipped

  • Sold-out variants show an alert form. Repeat submissions for the same variant and address stay one record, and only a masked address is kept.
  • The route verifies the HMAC over the raw body, claims the event, then updates the waiting subscriptions.
  • On failure it releases the claim and returns 500, so Shopify's retry handles only the subscriptions still waiting.

How it was checked

  • A wrong signature and a tampered body both return 401.
  • One event delivered twice concurrently and replayed once produces a single event record.
  • Raising a live sold-out variant by 1 sent a real webhook that marked the subscription notified within 6 seconds. Stock was then set back to 0.

What it costs

  • No real email is sent. Sending needs a mail provider, full addresses and an unsubscribe flow.
  • Each event handles at most 50 subscriptions.
  • Event records have no clean-up job and keep accumulating.

How to read this

The live-store cases leave out the brand, internal data and figures. PORTVA is a concept store with test checkout and no real traffic, so its results are test runs and lab measurements. Source code stays private, and any of this can be walked through in an interview.