giftsngifts.in
GiftsNGifts

GiftsNGifts

A complete multi-vendor commerce platform where gifting works for every side - buyers discover, sellers run their own catalog, and operations never sees a manual step.

Timeline
2024 - 25
Role
Full-stack engineer
Team
Team
Status
live

Technology Stack

ReactReactViteViteNode.jsNode.jsExpressExpressMongoDBMongoDBRazorpayRazorpayCloudinaryCloudinaryDelhiveryTailwind CSSTailwind CSS

✦Overview

GiftsNGifts is a production multi-vendor gifting marketplace serving real buyers and sellers at giftsngifts.in. Three fundamentally different user experiences - customer storefront, seller hub, admin panel - each get a purpose-built React app consuming one shared Express API. Collapsing them into a role-switched UI would compromise all three.

The backend is a serious commerce engine: Razorpay handles payments with stock reservation and idempotency so the same webhook can replay safely; Delhivery takes care of logistics end-to-end; Cloudinary serves product media from CDN. On top sits a B2B layer for corporate gifting - bulk quotes, bulk carts and a separate approval pipeline that retail buyers never see.

✦Key Features

  • Multi-vendor marketplace - Buyers discover and purchase gifts; sellers manage their own catalog, inventory and orders through a dedicated hub - no admin mediation needed for daily operations.

  • Razorpay + idempotent checkout - Stock is reserved at checkout creation and confirmed only after Razorpay webhook verification - no double-charges on network retry, no overselling under concurrent load.

  • Delhivery logistics integration - Orders are automatically pushed to the Delhivery API after payment confirmation - shipping labels, tracking and status updates flow back into the platform without manual intervention.

  • Cloudinary media pipeline - Sellers upload original product images; the platform delivers optimised, CDN-served variants. No manual image processing, no bandwidth bill from serving originals.

  • B2B bulk ordering - Corporate clients can raise bulk quote requests and bulk cart orders - a separate flow from retail with its own pricing logic, catalog and approval pipeline.

✦Technical Implementation

01Idempotent payment - reserve, verify, confirm

Checkout is a three-step atomic sequence: stock is reserved at order creation (PendingOrder saved with full shipping and item data), Razorpay processes payment client-side, and the webhook fires a server-side verify call. The verify path acquires an idempotency lock before touching the database - a replayed webhook resolves to the same order without creating a duplicate.

paymentController.js
js
// Step 2: Webhook verify - idempotent, runs exactly once per payment
export const paymentVerification = async (req, res) => {
  const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body;

  // Acquire lock - second call on same ID returns 409 immediately
  const lock = await acquireIdempotencyLock(razorpay_order_id);
  if (!lock) return res.status(409).json({ error: "Payment already processing" });

  try {
    if (await isAlreadyProcessed(razorpay_order_id))
      return res.json({ success: true, alreadyProcessed: true });

    // HMAC signature check - rejects tampered webhooks
    const hmac = crypto.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET);
    hmac.update(`${razorpay_order_id}|${razorpay_payment_id}`);
    if (hmac.digest("hex") !== razorpay_signature)
      return res.status(400).json({ error: "Signature mismatch" });

    // Atomic stock confirmation + order state transition
    await confirmStockPurchase(pendingOrder.reservations);
    await transitionOrder(order, ORDER_STATES.CONFIRMED);
    await markProcessed(razorpay_order_id);
    completeIdempotencyLock(razorpay_order_id);
  } catch (err) {
    await releaseReservation(pendingOrder.reservations); // rollback on failure
    throw err;
  }
};

02Data model - 38 Mongoose schemas across five layers

Each layer has strict ownership scoping. A seller can never read another seller's orders or payout ledger - the model boundaries enforce it without per-query guards.

DomainModelsScope
CatalogProduct, Category, Subcategory, GiftOption, Occasion, Artisan, CraftMulti-vendor product listings + taxonomy
CommerceCart, Order, PendingOrder, Payment, Payout, BulkCart, BulkQuoteRetail + B2B purchase flow
UsersUser, Seller, Admin, UserProfile, BankDetailsBuyer + seller + ops identities
LogisticsWarehouse, ShippingSettings, StateDelhivery integration + delivery zones
PlatformNotification, Support, Review, Coupon, PersonalizationModelCX and engagement layer

03Server architecture

37 route files, 30 controllers, a request-correlation middleware and express-mongo-sanitize on every incoming document. The same defence-in-depth pattern applied to DealDirect - justified here because GiftsNGifts is also a real-money platform with real seller payouts.

✦Project Structure

structure
giftsngifts/
├─ Client/          # Vite + React storefront
├─ Seller/          # Vite + React seller hub
├─ Admin/           # Vite + React admin panel
└─ Server/
   ├─ server.js     # Express — helmet, cors, mongo-sanitize
   ├─ controller/   # 30 controllers
   ├─ routes/       # 37 route files
   ├─ model/        # 38 Mongoose models
   └─ services/     # stockReservation, idempotency, notifications

✦Design Decisions

Separate app per audience

A customer discovering gifts has nothing in common with a seller managing stock or an admin handling disputes. Three purpose-built UIs on one stable API contract means each interface evolves independently - and seller routes never leak into the buyer bundle.

PendingOrder + idempotency service

Saving the full order payload (items, shipping address, gift messages) at checkout creation - before payment - means the webhook verify path doesn't need a second client call. Stock is held for 15 minutes; on payment success the pending order converts atomically. Idempotency keys on the webhook prevent any retry from double-creating.

Lazy Razorpay instantiation

ESM imports are hoisted before dotenv.config() runs, so Razorpay credentials aren't available at module load time. Deferring instantiation to first use with a cached singleton sidesteps the race without restructuring the entry point.

✦Challenges

↳

Stock races under concurrent checkout

Two buyers checking out the last unit simultaneously could both reserve and both confirm. Solved with a reserve-then-confirm flow where stock only decrements in confirmStockPurchase if the reservation is still valid and the idempotency lock is held.

↳

Multi-vendor payout ledger

Sellers earn from orders they fulfil; calculating payouts across a mixed-seller cart requires isolating each seller's contribution to an order, recording it in their ledger, and running a settlement job that never leaks cross-vendor data.

↳

Webhook replay attacks

Razorpay replays webhooks on network failure. Without idempotency keys, a replayed payment.captured event would create a second order for the same payment ID. The idempotency service now marks each razorpay_order_id as processed before creating the real order.

Want the full walkthrough - architecture, decisions, war stories?

ask me about it