X-Drop is a SaaS management platform for dropshipping operations. In just 2 months of operation, we reached R$ 30k+ in processed revenue, 100+ active users, 400+ orders and an MRR around R$ 20k. In this case study, I'll share the architecture decisions, real technical challenges and trade-offs we faced to reach these numbers.
The problem
Dropshipping sellers operate across multiple marketplaces simultaneously — Mercado Livre, Shopee, among others. Each platform has its own API, its own order flows, and its own data formats. Without a centralized tool, a seller needs to switch between 3-4 different dashboards, reconcile inventory manually and deal with multiple payment gateways. It doesn't scale.
X-Drop solves this: a single dashboard that integrates catalog, orders, shipping, payments and real-time financial reports — with role-based access control for teams.
Architecture and stack
The stack was chosen based on two criteria: iteration speed (we needed to ship fast) and ability to scale without rewriting everything in 6 months.
- Frontend: React + Next.js with TypeScript — SSR for public panel SEO, CSR for the internal dashboard
- Backend: NestJS (Node.js) with REST API — isolated modules per domain (orders, catalog, finance, integrations)
- Database: PostgreSQL + Firebase (auth and real-time listeners)
- Infra: AWS with Docker containers, automated CI/CD
- Payments: Asaas (PIX, bank slip) + Mercado Pago (card, PIX)
- Marketplaces: Mercado Livre API + Shopee API
Challenge #1: multi-marketplace integration
Each marketplace has a completely different API. Mercado Livre uses OAuth 2.0 with short-lived tokens and webhooks for notifications. Shopee has its own authentication system with HMAC signature. Order formats, statuses and categories are incompatible with each other.
The solution was to create a per-marketplace abstraction layer — adapters that normalize data to a unified internal schema. Each adapter implements the same interface (syncProducts, syncOrders, updateInventory), but internally handles each API's specifics.
// Common interface for all marketplaces
interface MarketplaceAdapter {
syncProducts(sellerId: string): Promise<Product[]>
syncOrders(sellerId: string, since: Date): Promise<Order[]>
updateInventory(productId: string, quantity: number): Promise<void>
mapStatus(externalStatus: string): InternalOrderStatus
}
// Each marketplace implements its own version
class MercadoLivreAdapter implements MarketplaceAdapter {
async syncOrders(sellerId: string, since: Date) {
const token = await this.refreshToken(sellerId)
const raw = await this.api.get('/orders/search', { seller: sellerId, since })
return raw.results.map(order => this.normalizeOrder(order))
}
}
class ShopeeAdapter implements MarketplaceAdapter {
async syncOrders(sellerId: string, since: Date) {
const signature = this.generateHMAC(sellerId, timestamp)
const raw = await this.api.get('/order/get_order_list', { sign: signature })
return raw.order_list.map(order => this.normalizeOrder(order))
}
}This pattern allowed us to add new marketplaces without touching the application core. When a marketplace changes its API (which happens frequently), the impact stays contained within the adapter.
Challenge #2: multiple payment gateways
We needed to support Asaas and Mercado Pago simultaneously — each seller can choose their preferred gateway. The challenges:
- Different webhooks: each gateway notifies in distinct formats with different delivery guarantees
- Idempotency: a single payment can trigger multiple webhooks (gateway retries). Without control, you process the same payment twice
- Reconciliation: the balance reported by the gateway doesn't always match what you calculated internally
- PIX: asynchronous flow with expiration window — status changes from 'pending' to 'paid' or 'expired' via webhook
The solution followed the same principle as marketplaces: adapter pattern + a payment events table with an idempotency key. Each incoming webhook is registered with a unique hash. If the same event arrives twice, the second one is discarded before any processing.
async function handlePaymentWebhook(provider: string, payload: unknown) {
const adapter = getPaymentAdapter(provider) // 'asaas' | 'mercadopago'
const event = adapter.parseWebhook(payload)
// Idempotency: check if this event was already processed
const idempotencyKey = `${provider}:${event.externalId}:${event.type}`
const exists = await db.paymentEvent.findUnique({ where: { idempotencyKey } })
if (exists) return { status: 'already_processed' }
// Register the event and process
await db.$transaction(async (tx) => {
await tx.paymentEvent.create({ data: { idempotencyKey, ...event } })
await tx.order.update({
where: { id: event.orderId },
data: { paymentStatus: event.status },
})
})
}Challenge #3: scale and observability
With 100+ users and 400+ orders in 2 months, we started feeling the first signs that architecture decisions matter. Report queries that took 50ms started taking 800ms. Marketplace syncs running in the background started competing for database connection pool slots.
Actions we took:
- Composite indexes on order and transaction tables — report queries went back to < 100ms
- Connection pooling with separate limits for synchronous operations (API) and asynchronous ones (marketplace sync)
- Rate limiting per seller on marketplace API calls — prevents a seller with a large catalog from consuming the entire API quota
- Structured logging with operation context (sellerId, marketplace, orderId) — without this, debugging a production issue across 3 marketplaces and 2 gateways is impossible
- Health checks with per-integration latency metrics — if Mercado Livre response time exceeds 2s, we get an alert before the user complains
Challenge #4: governance and multi-tenancy
X-Drop serves sellers with teams. A store owner needs to give employees access to process orders, but without exposing financial data or integration settings. We implemented RBAC (Role-Based Access Control) with 3 levels: admin, operator and viewer.
Every request passes through middleware that validates the tenant (seller) and user role. Data is filtered by sellerId in every query — there's no database call that doesn't pass through this filter. This guarantees complete isolation between sellers.
Results in 2 months
- R$ 30k+ in revenue processed through the platform
- 100+ active users
- 400+ orders processed
- R$ 20k MRR — validating product-market fit
- 2 marketplaces integrated (Mercado Livre + Shopee)
- 2 payment gateways (Asaas + Mercado Pago)
- Zero downtime since launch
Lessons learned
- Adapter pattern is essential when integrating with external systems that change without notice. Investing time in abstraction early saved weeks later
- Idempotency is not optional — with payment webhooks, it's the difference between charging the customer once or twice
- Observability from day 1. When Mercado Livre changed a field format without documenting it, our structured logs showed exactly which field broke, for which seller, in which order. Without that, it would have been hours of debugging
- Scale isn't just infra. The first bottlenecks were poorly indexed queries and misconfigured connection pooling — application-level problems, not server problems
Final stack
- React + Next.js + TypeScript (frontend)
- NestJS (API backend)
- PostgreSQL + Firebase (database and auth)
- Docker + AWS (infrastructure)
- Asaas + Mercado Pago (payments)
- Mercado Livre API + Shopee API (marketplaces)
- CI/CD with automated deploys
--- Want to learn more about the project, its purpose and see the platform? Visit the X-Drop dedicated page.
