Vinicius Aguiar
Case Study

Case Study: X-Drop — how I built a dropshipping SaaS that generated R$ 30k+ in 2 months

Apr 16, 2026 · 12 min read

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:

  1. Composite indexes on order and transaction tables — report queries went back to < 100ms
  2. Connection pooling with separate limits for synchronous operations (API) and asynchronous ones (marketplace sync)
  3. Rate limiting per seller on marketplace API calls — prevents a seller with a large catalog from consuming the entire API quota
  4. Structured logging with operation context (sellerId, marketplace, orderId) — without this, debugging a production issue across 3 marketplaces and 2 gateways is impossible
  5. 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

  1. Adapter pattern is essential when integrating with external systems that change without notice. Investing time in abstraction early saved weeks later
  2. Idempotency is not optional — with payment webhooks, it's the difference between charging the customer once or twice
  3. 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
  4. 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.