Skip to content

Collector marketplace on Node.js and React

Pokémon Card Trading Platform with Shopify Integration

A trading and catalogue platform for card collectors, integrated with Shopify for commerce and built to handle inconsistent third-party card data.

Commerce
Shopify Admin API
Catalogue
Normalised card data
Sync
Idempotent webhooks
Status
Live demo available

Role

Full-stack development and integration

Timeline

2026

Stack

Node.js, React, MongoDB, Shopify API

Overview

A platform for trading-card collectors: browse a normalised card catalogue, track a personal collection, and buy through a Shopify-backed storefront. React on the front end, Node.js and MongoDB behind it, with Shopify handling checkout, payment and fulfilment.

The problem

Two things make this harder than a standard catalogue app. First, trading-card data from third-party sources is genuinely messy — the same card appears across sets, printings, languages and condition grades, with inconsistent naming between providers. Second, commerce state lives in Shopify while catalogue and collection state lives in the application database, and those two have to agree.

Architecture

Shopify is the source of truth for anything involving money: products, prices, inventory, orders. The application database owns catalogue metadata and user collections. A single reference field links a catalogue entry to its Shopify product.

That split matters. Reimplementing checkout, tax or payment handling would have been substantial work with real compliance surface, and Shopify does it properly. The integration boundary is deliberately narrow: the app never writes prices or inventory, only reads them.

Technical challenges

Normalising inconsistent card data

Card identity is not a single field. The same card differs by set, collector number, printing variant, language and grade, and source data spells all of those inconsistently.

The solution was a deterministic composite key derived from the stable identifying attributes, with a normalisation step applied at ingest — casing, punctuation and known naming variants folded before hashing. Re-ingesting the same source data produces the same keys, so imports are repeatable rather than duplicating the catalogue.

Keeping Shopify and the catalogue in sync

Polling the Shopify API for changes is slow and burns rate limit. Webhooks are the right mechanism, but they arrive at-least-once and occasionally out of order, so a naive handler can apply a stale update over a fresh one.

Handlers are idempotent and version-aware: each applies only if the incoming payload is newer than what is stored, and delivery IDs are recorded so replays are ignored. Webhook signatures are verified before the body is parsed.

ts
// Verify before trusting anything in the payload.
function verifyShopifyWebhook(raw: Buffer, header: string) {
  const digest = crypto
    .createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET!)
    .update(raw)
    .digest("base64");

  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(header));
}

Using a timing-safe comparison rather than `===` matters here — a plain string comparison leaks signature information through response timing.

Search across a large catalogue

Collectors search by partial name, set, and attributes together, and expect it to feel instant. Regex scans over the collection did not hold up as the catalogue grew.

Moving to a compound text index with the common filter fields included let MongoDB serve the frequent query shapes from the index, and paginating by indexed cursor rather than by skip kept later pages as fast as the first.

Outcome

The platform runs as a live demo with a working catalogue, collection tracking and Shopify-backed purchasing. The architectural decision worth highlighting is the narrow integration boundary — by refusing to duplicate commerce state locally, an entire class of reconciliation bug never existed.

Screens

Card catalogue browse view
Card detail page
Collection management screen
Mobile catalogue interface

Published by Cenedy Udoy Palma.

Other case studies