Back to Projects

Primsell NFT E-Commerce

Node.js Business Logic on PostgreSQL — Checkout, Stripe Connect & Royalty Ledger

Overview

Role: Main full-stack developer in a team of 3–5: backend business logic in Node.js on PostgreSQL (Express/Sequelize core API, NestJS payment service), the blockchain background jobs and the React apps. Mentored 2 junior engineers.

Main full-stack developer on a B2B NFT-commerce platform: Node.js business logic on PostgreSQL (Express/Sequelize core API, NestJS Stripe Connect payment service), blockchain background jobs on Polygon, and React apps for brands, checkout, redeem and venue verification sharing one UI kit and API client. Mentored 2 junior engineers.

Key Features

  • Express + Sequelize core API on PostgreSQL (~79 REST endpoints, JWT + ACL roles, validation and rate limits per route)
  • NestJS + MikroORM payment service on Stripe Connect: per-brand accounts, application fees, signed webhooks, decimal.js money math
  • Checkout overselling fixed with SELECT … FOR UPDATE on the campaign row and atomic SQL counters
  • Royalty ledger fed by an OpenSea/Rarible resale indexer (checkpoints, overlap re-scan, dedup) with confirmation-gated withdrawals
  • Idempotent burn-to-redeem on the transaction hash, plus a recovery job for interrupted redeems
  • State-machine workflow engine for orders, ledger operations and contract deployment; cron jobs with a DB-backed job log

Tech Stack

Backend

Node.jsTypeScriptExpressSequelizeNestJSMikroORMPostgreSQLRedis

Frontend

React 18Redux Toolkitredux-sagaReact QueryMaterial UIwagmiWalletConnect

Payments & Blockchain

Stripe Connectdecimal.jsethers.jsweb3.jsPolygonAlchemyOpenSea & Rarible APIs

Infra

DockerAWS (S3, ECS)GitHub ActionsSentry

Challenges & Solutions

Overselling Under Concurrent Checkout

Problem

Order creation read the campaign, checked stock in application memory, then wrote the new booked count back. Two concurrent buyers could both pass the check, one increment was lost, and more NFTs were booked than existed.

Solution

One READ COMMITTED transaction takes SELECT … FOR UPDATE on the campaign row, checks stock under the lock, inserts the order and bumps the booked count with an atomic SQL update. Release and booked → bought transfers are atomic updates too, and available stock is a derived value that cannot be written directly.

Late Payments vs Order Expiry

Problem

Orders hold stock for a limited time. A buyer who paid just after the reservation expired could be charged for an order the system had already released, and amounts computed as floats broke on prices like 19.99.

Solution

The expiry job asks Stripe for the PaymentIntent status before expiring an order: paid orders complete, the rest release stock in bulk and cancel their intent so they can never be charged. Money moved to decimal.js minor units, with the platform fee rounded down so it never exceeds the percentage.

Royalty Ledger From On-Chain Resales

Problem

Brands earn royalties when their NFTs resell on OpenSea or Rarible, and must be able to withdraw them — without missing events near the chain head or counting a sale twice.

Solution

A resale indexer reads block windows from a stored checkpoint, re-scans an overlap, halves the range on provider errors and dedupes by transaction. It feeds a per-brand, per-currency ledger of signed operations whose lifecycle runs on the workflow engine, with a balance guard on withdrawals and N block confirmations before completion.

Redeems That Must Not Be Lost

Problem

A buyer burns the NFT on-chain to get a real-world code. If the browser closed after signing, the burn happened but the backend never heard about it.

Solution

The client records a redeem intent before signing. The redeem endpoint verifies the receipt and the decoded event and is idempotent on the transaction hash, and a recovery job scans chain events from the intent's block to complete redeems whose HTTP call never arrived.

Brand ↔ Customer CRM Mapping

Problem

Brands must see only their own customers, while one wallet can buy from many brands — and the mapping had to cover past purchases too.

Solution

A many-to-many table with a composite key, filled by a BEFORE INSERT trigger on purchases with ON CONFLICT DO NOTHING, plus a separate idempotent backfill migration for the history.

Key Achievements

Row-Locked Checkout
SELECT … FOR UPDATE + atomic counters stopped overselling
Stripe Connect
Per-brand accounts, application fees, signed webhooks, decimal money math
Royalty Ledger
OpenSea/Rarible resale indexer with confirmation-gated withdrawals
Mentored 2
Junior engineers on the team