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
✦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.
// 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.
| Domain | Models | Scope |
|---|---|---|
| Catalog | Product, Category, Subcategory, GiftOption, Occasion, Artisan, Craft | Multi-vendor product listings + taxonomy |
| Commerce | Cart, Order, PendingOrder, Payment, Payout, BulkCart, BulkQuote | Retail + B2B purchase flow |
| Users | User, Seller, Admin, UserProfile, BankDetails | Buyer + seller + ops identities |
| Logistics | Warehouse, ShippingSettings, State | Delhivery integration + delivery zones |
| Platform | Notification, Support, Review, Coupon, PersonalizationModel | CX 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
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

