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