X-Drop es una plataforma SaaS de gestión para operaciones de dropshipping. En solo 2 meses de operación, alcanzamos R$ 30k+ en facturación procesada, 100+ usuarios activos, 400+ pedidos y un MRR en la casa de R$ 20k. En este case study, compartiré las decisiones de arquitectura, los desafíos técnicos reales y los trade-offs que enfrentamos para llegar a estos números.
El problema
Los vendedores de dropshipping operan en múltiples marketplaces simultáneamente — Mercado Livre, Shopee, entre otros. Cada plataforma tiene su propia API, sus propios flujos de pedido y sus propios formatos de datos. Sin una herramienta centralizada, el vendedor necesita alternar entre 3-4 paneles diferentes, conciliar inventario manualmente y lidiar con múltiples gateways de pago. No escala.
X-Drop resuelve esto: un único panel que integra catálogo, pedidos, envíos, pagos y reportes financieros en tiempo real — con control de acceso por roles para equipos.
Arquitectura y stack
El stack fue elegido con dos criterios: velocidad de iteración (necesitábamos lanzar rápido) y capacidad de escalar sin reescribir todo en 6 meses.
- Frontend: React + Next.js con TypeScript — SSR para SEO del panel público, CSR para el dashboard interno
- Backend: NestJS (Node.js) con API REST — módulos aislados por dominio (pedidos, catálogo, financiero, integraciones)
- Base de datos: PostgreSQL + Firebase (auth y real-time listeners)
- Infra: AWS con contenedores Docker, CI/CD automatizado
- Pagos: Asaas (PIX, boleto) + Mercado Pago (tarjeta, PIX)
- Marketplaces: Mercado Livre API + Shopee API
Desafío #1: integración multi-marketplace
Cada marketplace tiene una API completamente diferente. Mercado Livre usa OAuth 2.0 con tokens de corta duración y webhooks para notificaciones. Shopee tiene su propio sistema de autenticación con firma HMAC. Los formatos de pedido, estados y categorías son incompatibles entre sí.
La solución fue crear una capa de abstracción por marketplace — adapters que normalizan los datos hacia un schema interno unificado. Cada adapter implementa la misma interfaz (syncProducts, syncOrders, updateInventory), pero internamente maneja las particularidades de cada API.
// Interfaz común para todos los 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
}
// Cada marketplace implementa su propia versión
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))
}
}Este patrón nos permitió agregar nuevos marketplaces sin tocar el core de la aplicación. Cuando un marketplace cambia su API (lo que sucede con frecuencia), el impacto queda contenido en el adapter.
Desafío #2: múltiples gateways de pago
Necesitábamos soportar Asaas y Mercado Pago simultáneamente — cada vendedor puede elegir su gateway preferido. Los desafíos:
- Webhooks diferentes: cada gateway notifica en formatos distintos y con diferentes garantías de entrega
- Idempotencia: un mismo pago puede generar múltiples webhooks (retry del gateway). Sin control, procesas el mismo pago dos veces
- Conciliación: el saldo reportado por el gateway no siempre coincide con lo que calculaste internamente
- PIX: flujo asíncrono con ventana de expiración — el estado cambia de 'pending' a 'paid' o 'expired' vía webhook
La solución siguió el mismo principio de los marketplaces: adapter pattern + una tabla de eventos de pago con clave de idempotencia. Cada webhook recibido se registra con un hash único. Si el mismo evento llega dos veces, el segundo se descarta antes de cualquier procesamiento.
async function handlePaymentWebhook(provider: string, payload: unknown) {
const adapter = getPaymentAdapter(provider) // 'asaas' | 'mercadopago'
const event = adapter.parseWebhook(payload)
// Idempotencia: verifica si este evento ya fue procesado
const idempotencyKey = `${provider}:${event.externalId}:${event.type}`
const exists = await db.paymentEvent.findUnique({ where: { idempotencyKey } })
if (exists) return { status: 'already_processed' }
// Registra el evento y procesa
await db.$transaction(async (tx) => {
await tx.paymentEvent.create({ data: { idempotencyKey, ...event } })
await tx.order.update({
where: { id: event.orderId },
data: { paymentStatus: event.status },
})
})
}Desafío #3: escala y observabilidad
Con 100+ usuarios y 400+ pedidos en 2 meses, comenzamos a sentir las primeras señales de que las decisiones de arquitectura importan. Queries de reportes que tomaban 50ms empezaron a tomar 800ms. Sincronizaciones de marketplace que corrían en background empezaron a competir por conexiones del pool de base de datos.
Las acciones que tomamos:
- Índices compuestos en las tablas de pedidos y transacciones — queries de reportes volvieron a < 100ms
- Connection pooling con límites separados para operaciones síncronas (API) y asíncronas (sync de marketplace)
- Rate limiting por seller en llamadas a marketplaces — evita que un vendedor con catálogo grande consuma toda la cuota de la API
- Logs estructurados con contexto de operación (sellerId, marketplace, orderId) — sin esto, debuggear un problema en producción entre 3 marketplaces y 2 gateways es imposible
- Health checks con métricas de latencia por integración — si el tiempo de respuesta de Mercado Livre pasa de 2s, recibimos alerta antes de que el usuario se queje
Desafío #4: gobernanza y multi-tenancy
X-Drop atiende vendedores con equipos. Un dueño de tienda necesita dar acceso a empleados que procesan pedidos, pero sin exponer datos financieros o configuraciones de integración. Implementamos RBAC (Role-Based Access Control) con 3 niveles: admin, operador y visualizador.
Cada request pasa por un middleware que valida el tenant (seller) y el rol del usuario. Los datos se filtran por sellerId en toda query — no existe llamada a la base de datos que no pase por este filtro. Esto garantiza aislamiento total entre vendedores.
Resultados en 2 meses
- R$ 30k+ en facturación procesada por la plataforma
- 100+ usuarios activos
- 400+ pedidos procesados
- MRR de R$ 20k — validando product-market fit
- 2 marketplaces integrados (Mercado Livre + Shopee)
- 2 gateways de pago (Asaas + Mercado Pago)
- Zero downtime desde el lanzamiento
Lecciones aprendidas
- Adapter pattern es esencial cuando integras con sistemas externos que cambian sin aviso. Invertir tiempo en la abstracción al inicio ahorró semanas después
- Idempotencia no es opcional — con webhooks de pago, es la diferencia entre cobrar al cliente una vez o dos veces
- Observabilidad desde el día 1. Cuando Mercado Livre cambió el formato de un campo sin documentar, nuestros logs estructurados mostraron exactamente qué campo se rompió, en cuál seller, en cuál pedido. Sin eso, habrían sido horas de debug
- Escala no es solo infra. Los primeros cuellos de botella fueron queries mal indexadas y pool de conexiones mal configurado — problemas de aplicación, no de servidor
Stack final
- React + Next.js + TypeScript (frontend)
- NestJS (API backend)
- PostgreSQL + Firebase (base de datos y auth)
- Docker + AWS (infraestructura)
- Asaas + Mercado Pago (pagos)
- Mercado Livre API + Shopee API (marketplaces)
- CI/CD con deploys automatizados
--- ¿Quieres conocer más sobre el proyecto, su propósito y ver la plataforma? Accede a la página dedicada de X-Drop.
