Lessons from Shipping a Multi-Vendor Marketplace to Production

Source: Dev.to | Jan 27, 2026 (UTC)

I recently shipped a multi-vendor marketplace platform with vendor subdomains, payment processing, and file uploads. During development and deployment, I ran into several issues that weren’t obvious from tutorials or documentation. Here’s what happened and how I solved them.

The Setup

Frontend on Vercel, backend on Fly.io, vendors get their own subdomains. The architecture worked well, but production revealed edge cases I hadn’t considered during local development.

1. CORS Configuration for Wildcard Subdomains

What Happened:

Vendor subdomains like vendorname.example.com were getting blocked when making API requests:

Access to fetch at 'https://api-backend.fly.dev/api/vendor/products' from origin 'https://vendorname.example.com' has been blocked by CORS policy.

Enter fullscreen mode

Exit fullscreen mode

The issue was that my initial CORS setup only whitelisted the main domain, not the subdomain pattern.

The Fix:

Updated backend CORS to whitelist both the main domain and wildcard subdomains:

https://example.com https://*.example.com

Enter fullscreen mode

Exit fullscreen mode

Made sure credentials: true was set along with the necessary methods and headers.

Takeaway:

Dynamic subdomains need explicit CORS patterns. This is common in multi-tenant applications but easy to miss until you test in production with real subdomains.

2. Data Structure Mismatch in Image Uploads

What Happened:

The backend schema expected images: [String], but during testing I noticed the frontend was sending:

[{ url: null, publicId: undefined, isPrimary: true }]

Enter fullscreen mode

Exit fullscreen mode

This caused MongoDB CastErrors that broke product creation.

The Fix:

Normalized the data on the frontend before sending:

submitData.images = imageUrls.map(img => img.url).filter(Boolean);

Enter fullscreen mode

Exit fullscreen mode

The backend image upload endpoint now returns string[] for database insertion while keeping richer metadata for UI state management.

Takeaway:

When building APIs, I’ve learned to validate data shape at both ends. Frontend state often needs more metadata than the database requires, so mapping before submission prevents type errors.

3. API Response Inconsistency

What Happened:

After creating vendor profiles, the database showed correct values but the API response was returning hardcoded defaults:

{ "canReceiveOrders": false, "isPublic": false }

Enter fullscreen mode

Exit fullscreen mode

This created confusion in the UI, showing stores as inactive when they were actually live.

The Fix:

Changed the response to reflect actual database state:

res.status(201).json({ success: true, message: 'Vendor profile created successfully! Your store is now live.', data: { vendor, approvalStatus: vendor.approvalStatus, canReceiveOrders: vendor.isVerified, isPublic: vendor.isPublic } });

Enter fullscreen mode

Exit fullscreen mode

Takeaway:

Response payloads should always reflect database state, not assumptions. This seems obvious but during rapid iteration it’s easy to return static values and forget to update them.

4. Payment Webhook Security Gap

What Happened:

While reviewing the payment flow, I realized the webhook endpoint was accepting POST requests without verifying they actually came from Stripe. This is a common attack vector.

The Fix:

Implemented signature verification:

const sig = req.headers['stripe-signature']; event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);

Enter fullscreen mode

Exit fullscreen mode

Takeaway:

Payment webhooks are critical infrastructure. Signature verification should be implemented from day one, not added later. I’ve made it part of my initial setup checklist now.

5. Missing Authorization on Product Updates

What Happened:

The route /products/:id/stock verified that a vendor was logged in, but didn’t check if they owned the product. This meant any vendor could potentially modify another vendor’s inventory.

The Fix:

Added ownership verification middleware:

router.patch('/products/:id/stock', protect, authorize('vendor'), attachVendor, validateMongoId('id'), updateStock);

Enter fullscreen mode

Exit fullscreen mode

The attachVendor middleware ensures requests only succeed if the product belongs to the authenticated vendor.

Takeaway:

Authentication confirms who you are, authorization confirms what you can access. For multi-tenant systems, every resource route needs ownership checks. IDOR vulnerabilities are common when building quickly.

6. Deployment Cache Issues on Vercel

What Happened:

After pushing updates, the production site wasn’t reflecting new code. Features that worked locally, including the fixes above, weren’t showing up.

The Fix:

Forced a clean deployment:

vercel --prod --force

Enter fullscreen mode

Exit fullscreen mode

This bypassed caches and deployed fresh. Everything worked after that.

Takeaway:

Platform-specific quirks exist. Vercel, Netlify, and similar services cache aggressively. When behavior doesn’t match your local environment, a forced rebuild often resolves it.

Quick Reference

Issue

Cause

Solution

Why It Matters

CORS blocking subdomains

Wildcard pattern not configured

Whitelist https://*.example.com

Common in multi-tenant apps

Image upload type error

Frontend/backend schema mismatch

Normalize data before sending

Prevents runtime errors

Response inconsistency

Hardcoded response values

Return actual DB state

Keeps UI in sync

Vendor IDOR vulnerability

Missing ownership check

Add authorization middleware

Critical for multi-tenant security

Webhook security gap

No signature verification

Verify Stripe signatures

Prevents payment fraud

Stale deployments

Build cache

Force clean deploy

Platform-specific behavior

What I’d Do Differently Next Time

These weren’t beginner mistakes; they’re the kind of issues that surface when shipping real applications with multiple user roles, payment processing, and complex domain setups. Documenting them helps me build faster next time and hopefully helps other developers avoid the same debugging sessions.

Enter fullscreen mode

Exit fullscreen mode

For further actions, you may consider blocking this person and/or reporting abuse

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Читайте також