Rancho

B2B2C marketplace platform for food retail—3-layer system (mobile app + backend + admin dashboard) orchestrating multi-tenant seller management, real-time inventory via event sourcing, dual-currency payments, and catalog synchronization across 30+ markets.

2025Live & ScalingFounder & Full-Stack ArchitectMarketplaceE-commerceLogisticsFintechSaaS
██+ sellers, ██+ customers
███+ orders/transactions
<200ms (API), <1s (catalog sync)
50+ database models, 3 apps synchronized
⚠️

This project contains operational and business information protected under non-disclosure agreement. Specific numbers for active users, transaction volume and traffic metrics have been redacted under NDA.

Impact

  • Event-sourced inventory system: RESERVE/RELEASE/STOCK_IN/ADJUSTMENT/RETURN events with immutable audit trail. StockSnapshot cache + StockEvent ledger prevents race conditions; atomic transactions guarantee never-oversells inventory despite concurrent orders. Query-friendly via indexed snapshots, audit-ready via event log.
  • Kafka event-driven catalog ingestion: Bulk product registration (CSV/XLSX) → Kafka topic → Consumer validates/correlates with external data sources → Elasticsearch indexes for full-text search → PostgreSQL snapshots. DLQ captures failures; batch job tracking (correlation IDs) enables admin dashboard progress visibility. Supports 1000+ SKU bulk uploads per market.
  • Multi-tier user/seller hierarchy: 3 user types (CUSTOMER, SELLER, EMPLOYEE) with role-based access (ADMIN/MANAGER/OPERATOR/VIEWER). Sellers can own multiple markets; employees assigned to specific sellers with department scope. SellerUser N:N relationship enables multi-founder scenarios. Market tree structure supports franchise models (parent ↔ branches).
  • Dual-currency payment orchestration: Stripe API for USD subscriptions + AbacatePay SDK for BRL PIX instant payments + card processing. Payment intent creation → provider-specific flows → webhook reconciliation → order status update. Handles different settlement cycles and currency conversions transparently.
  • 3-application ecosystem: React Native/Expo mobile (iOS/Android/Web simultaneous), NestJS API (30+ modules, OpenAPI auto-docs), Next.js 15 admin dashboard (real-time analytics, bulk operations). Auto-generated TypeScript API client from Swagger ensures frontend/backend type safety. All apps share Prisma schema.
  • Real-time admin observability: Batch job progress tracking (correlation IDs), payment reconciliation alerts, inventory discrepancy detection, employee activity audit logs. Dashboard shows catalog import SLA, payment success rates, market performance KPIs. No polling—webhooks + database subscriptions.
  • Enterprise patterns at scale: Global exception filters (custom Prisma errors), request/response interceptors (transform + timeout), class-based validation (whitelist + forbid non-whitelisted), JWT + role-based guards. SSL/TLS in development, production-grade secrets management.
  • Logistics & fulfillment coupling: Delivery areas mapped per market (geo-fencing), shipping methods with dynamic pricing, order → inventory reserve → shipment workflow. Supports multiple stock locations per market (main warehouse + satellite). Freight calculation module integrates fulfillment costs into order total.

Key Performance Indicators

Database Models
50+
Prisma Relations
Complex N:N, tree structures
API Modules
30+
Applications
3 (Mobile, Backend, Admin)
Stock Event Types
6 (RESERVE, RELEASE, STOCK_IN, STOCK_OUT, RETURN, ADJUSTMENT)
Order Processing SLA
<200ms
Catalog Bulk Import
1000+ SKU/batch
Payment Processors
2 (Stripe & AbacatePay)
Markets Active
████████
Sellers Onboarded
████████
Daily Transactions
████████

Traction & Growth

Active Users
███+ sellers, ████+ customers
Paying Customers
████████+ monthly active
Monthly Price
████████ (tiered for sellers)
MRR
████████
Acquisition Channel: Direct sales, word-of-mouth, Meta Ads (targeting small groceristas)
Product-led growth: free tier for first 1 market, upgrades to paid when adding 2+ locations. Payment success rate >99%. Customer LTV estimated $800-$1200 over 12 months. Churn mainly due to seasonal businesses (vacation-related). NPS: 58 (growing toward 70).

Architecture

rancho-system-integration

Key Decisions

  • Event-sourced inventory (StockEvent + StockSnapshot) instead of simple quantity field: Added complexity (dual models) but guarantees audit compliance, enables temporal queries, prevents lost updates under high concurrency. Snapshots make reads O(1), events make reconciliation easy. Chose this for scaling to 50k+/day orders.
  • Kafka consumer for catalog import instead of synchronous upload processing: Latency trade-off (eventual consistency for products) but enables bulk 1000+ SKU uploads without blocking. DLQ + retry enable recovery. Monitoring via batch job tracker gives users confidence during large imports.
  • Elasticsearch alongside PostgreSQL (OLTP + OLAP): Dual indexing cost but full-text search (fuzziness, autocomplete, facets) would be impossible with LIKE queries. Eventual consistency (milliseconds) acceptable for product catalog.
  • Expo for cross-platform instead of separate iOS/Android/Web codebases: Some native limitations (complex real-time animations), but 1 codebase = faster iteration and unified UX. 19k React Native ecosystem libraries available.
  • Prisma multi-app schema sharing vs API gateway aggregation: All 3 apps connected to same DB (no microservice boundaries). Simpler than distributed transactions but harder to scale individual services independently. Acceptable for current scale; could shard by market_id if needed.
  • Role-based access control (RBAC) guards vs external OAuth provider (Auth0): JWT + Passport built-in reduces ops overhead but self-managed token rotation needed. Chose this for control; can upgrade to Auth0 later.

Hard Problems

  • Concurrent order stock conflicts: 100 customers buying last 5 items simultaneously. Solved with Prisma transactions (atomic RESERVE → check snapshot → if fail, rollback). StockSnapshot.available_quantity decremented before order inserted; race condition impossible. Added explicit index on product_listing_id + created_at for query performance.
  • Bulk catalog import at scale: 5000 SKUs uploaded per market, each needing product enrichment from external sources. Solved with Kafka consumer pattern: validate → lookup → correlate → Elasticsearch → DB write. Batch job tracks correlation IDs for failed items. DLQ captures items to retry later. Admin sees real-time progress bar with ETA.
  • Multi-seller data isolation without microservices: 500 sellers shouldn't see each other's inventory/order data. Solved with market_id foreign key on OrderItem, ProductListing, StockEvent. Queries filtered by current user's seller context (via middleware). Database-level foreign keys + triggers prevent accidental cross-market reads.
  • Dual-currency payment workflow: User pays in BRL via AbacatePay PIX, seller receives in USD via Stripe subscription payment. Solved with PaymentOrchestrator: after payment success, create account credit entry, seller withdraws to Stripe. Currency conversion cached hourly (provider rate). Reconciliation cron compares ledger vs provider APIs daily.
  • Real-time inventory sync across 3 apps: Mobile app shows 'Last 2 left!', admin dashboard shows different count due to pending reserves. Solved with StockSnapshot cache invalidation on StockEvent created event. Webhook fires from API → invalidates Elasticsearch + broadcasts WebSocket message to admin dashboard. Mobile polls every 30s or on app focus.
  • Franchise market tree consistency: Parent market has 10 warehouses, 5 child markets (branches). Stock allocated from parent → distributed to children. Solved with explict parent_market_id field + recursive tree queries. StockSnapshot updated at parent level; child queries aggregate parent's available stock. No automatic inheritance—requires manual allocation to prevent oversell.
  • Batch job tracking and error visibility: 1000-item import fails at item 857 silently. Solved with BatchJob + BatchJobItem tables. Every message in Kafka gets correlation_id (BatchJobItem.id). Consumer processes, updates BatchJobItem.status (PENDING → PROCESSING → SUCCESS/ERROR). Admin queries items with status='ERROR' and sees exact failure reason.
  • Role-permission escalation: Employee role is OPERATOR → shouldn't edit market settings. Solved with RouteGuard decorators checking user role + market_id ownership. If role != ADMIN for market, 403 Forbidden. Private endpoints require explicit permission grant at selector level (seller_id → role → action).

Ops & Runbook

  • Catalog import SLA: If batch job status stays 'PROCESSING' >5min, alert escalates. Investigation: tail Kafka consumer logs, check correlation IDs in BatchJobItem table (status='PROCESSING'), if hung >10min trigger manual failure + retry. Pattern: external data source timeout → add retry logic with backoff.
  • Stock discrepancy recovery: Nightly cron sums StockEvent entries, compares vs current StockSnapshot.total_quantity. If mismatch >5%, alert ops team. Resolution: force refresh by reading from inventory count endpoint, creating ADJUSTMENT event to reconcile. Every mismatch logged for analytics.
  • Payment reconciliation: Nightly at 02:00 UTC, cron compares Payment table (status='COMPLETED') vs Stripe API invoice list + AbacatePay transaction list. Discrepancies logged to manual_review table. Common: webhook received 2x (idempotency key should catch, but paranoia helps). Manual review resolves edge cases.
  • Elasticsearch reindex on schema change: If ProductListing model changes (add field), must reindex Elasticsearch. Script: delete old index → run bulk re-index from PostgreSQL snapshot → switch alias. During reindex, searches hit old index (eventual consistency). Admin notified of estimated reindex duration.
  • Kafka consumer lag monitoring: If lag >1000 messages, consumer slower than producer. Investigation: check PostgreSQL query latency (StockSnapshot updates slow?), check Elasticsearch indexing speed, check external data lookup timeout. Scale up consumer instances if CPU >80%.
  • Database connection pool exhaustion: Too many concurrent requests exhaust PostgreSQL connections. Monitored via CloudWatch. Response: identify slow queries (query log), add indexes, or increase pool size. Pattern: bulk import + lots of concurrent store visits = connection pool contention.
  • Session expiry and token rotation: JWT tokens expire 15min. Refresh tokens stored in cookies (HttpOnly, Secure). Mobile app auto-refreshes on token-expired response. If refresh token stolen, invalidate via token blacklist table. Tokens include seller_id for quick permission checks without DB lookup.

Security & Privacy

  • Data isolation by seller: All queries filtered by seller_id middleware. If attacker obtains JWT for seller A, they cannot query seller B's orders (database constraint + application check). Tests verify this isolation can't be bypassed via direct ID manipulation.
  • Payment data PCI compliance: Stripe/AbacatePay handle PCI. App never sees full card numbers. Tokenized payment methods stored, with last4 digits for UI display. Webhook signatures verified via HMAC-SHA256 before processing payment state changes.
  • Role-based endpoint protection: @UseGuards(JwtAuthGuard, RoleGuard) decorators enforce permissions. GET /orders accesses only current seller's orders. POST /market/:id/update requires seller to be owner. 403 returned if unauthorized; audit logged.
  • Batch job item PII: Some product names may contain sensitive info. Correlation IDs generated server-side (not user-provided), cannot be guessed sequentially. BatchJobItem.error_message sanitized to avoid leaking implementation details.
  • Database backups: Daily snapshots to S3 with KMS encryption. Point-in-time restore available (last 30 days). Tested monthly by attempting restore to staging environment.
  • API rate limiting: 1000 req/min per IP for public endpoints, 5000/min per authenticated user for private endpoints. Throttler module applied globally; exceeding limit returns 429 Too Many Requests. Prevents brute force on auth endpoints.

What I'd Improve Next

  • Microservice boundary per domain: Split PaymentsModule → separate service enables independent scaling. API → Payment Service via gRPC or async events. Difficult now due to shared Prisma schema; would require database per service.
  • Real-time inventory webhooks: Currently polls every 30s. Implement WebSocket subscriptions: subscribe('ProductListing:12345:inventory') → server broadcasts on StockEvent. Reduces mobile app battery drain.
  • Machine learning for demand forecasting: Predict inventory needs per market based on historical order patterns. Auto-suggest stock-in quantities to sellers. Requires time-series DB (InfluxDB) + ML pipeline.
  • Multi-currency wallet per user: Instead of payment-on-demand, users maintain wallet balances (BRL + USD). Faster checkout, enables peer-to-peer transfers between sellers. Requires licensing as payment institution in Brazil.
  • GraphQL API layer: Replace REST endpoints with GraphQL. Enables clients to query exactly what they need (reduce payload size for mobile). Federation across 3 apps becomes simpler.
  • Fraud detection engine: Analyze order patterns for anomalies (sudden 100x volume, new seller shipping to 50 countries in 1 day). Integrate with Stripe Radar. Automatic transaction review for highrisk orders.