← Projects

Rancho

Marketplace for small grocery stores with event-sourced inventory, Kafka for bulk catalog ingestion, PIX and Stripe payments. Three synchronized apps in production.

2025ProductionFounder & Full-Stack ArchitectMarketplaceE-commerceLogisticsFintechSaaS
  • NestJS
  • Prisma
  • PostgreSQL
  • Elasticsearch
  • Kafka
  • React Native
  • Expo
  • Next.js 15
  • Stripe
  • AbacatePay
  • Docker
  • Kubernetes
██+ vendors, ██+ customers
███+ orders/day
<200ms API, <1s catalog
50+ models, 3 apps
⚠️

Operational and commercial information protected by NDA. User metrics, transaction volume and MRR have been omitted.

Impact

  • Event-sourced inventory: RESERVE/RELEASE/STOCK_IN/ADJUSTMENT/RETURN events with StockSnapshot for fast reads and an event ledger for auditing. Atomic transactions ensure stock is never oversold, even under concurrent orders.
  • Kafka-based catalog ingestion: CSV/XLSX upload triggers a topic, consumer validates and indexes in Elasticsearch, DLQ captures failures. Admin sees real-time progress with per-batch correlation IDs. Supports 1000+ SKUs per upload.
  • Multi-level user hierarchy: three user types (CUSTOMER, SELLER, EMPLOYEE) with four-level RBAC. Sellers with multiple markets, employees with departmental scope. Tree structure for franchise chains.
  • Three applications sharing the same Prisma schema: React Native/Expo app for iOS, Android and Web, NestJS API with 30+ modules and auto-generated OpenAPI docs, and Next.js 15 admin dashboard with real-time analytics.

KPIs

Database models
50+
API modules
30+
Applications
3 (Mobile, Backend, Admin)
Stock event types
6
Order SLA
<200ms
Bulk import
1000+ SKU/batch
Processors
Stripe + AbacatePay
Active vendors
████████

Traction & Growth

Active Users
██+ vendors, ██+ customers
Paying Customers
████████
Monthly Price
████████ (tiered)
MRR
████████
Acquisition Channel
Direct sales, word-of-mouth, Meta Ads
Product-led growth: free tier for the first market, upgrade when adding more locations. Payment success rate above 99%. Estimated LTV R$ 4,000 to R$ 6,000 in 12 months. NPS: 58.

Architecture

rancho-system-integration

Key Decisions

  • Event sourcing for inventory instead of a simple quantity field: Two models instead of one, but guarantees auditability, enables temporal queries and prevents lost updates under concurrency. Snapshots keep reads O(1).
  • Kafka for catalog import instead of synchronous processing: Eventual consistency for products, but supports 1000+ SKU uploads without blocking. DLQ and retry give confidence. The admin progress bar shows exactly what is happening.
  • Elasticsearch alongside PostgreSQL: Double indexing has a cost, but full-text search with fuzziness and autocomplete would be impossible with LIKE. Eventual consistency in milliseconds is acceptable for a product catalog.
  • Expo for cross-platform instead of separate apps: Some native limitations, but a single codebase is much faster to iterate. The React Native ecosystem covers what we need.
  • Shared Prisma schema across all three apps: Simpler than distributed transactions, but scaling services independently is harder. Acceptable at current scale; sharding by market_id as an exit if needed.

Hard Problems

  • A hundred customers buying the last 5 units simultaneously. Prisma transactions with atomic RESERVE guarantee that StockSnapshot.available_quantity is decremented before the order is inserted. Race conditions impossible with the right index on product_listing_id + created_at.
  • 5000 SKUs from a single market with data enrichment from external sources. The Kafka consumer validates, looks up, correlates and indexes in Elasticsearch. BatchJob + BatchJobItem with correlation IDs let the admin see exactly which item failed and why.
  • 500 vendors without microservices: isolation via market_id in OrderItem, ProductListing and StockEvent. Middleware filters by current seller context. Database constraints and triggers prevent accidental cross-market reads.
  • User pays in BRL via AbacatePay PIX; vendor receives in USD via Stripe. PaymentOrchestrator creates a credit entry after success, vendor withdraws to Stripe. Exchange rate cached per hour. Daily cron reconciles ledger with provider APIs.
  • Mobile app showed different inventory counts than the admin dashboard due to pending reserves. StockSnapshot cache invalidated on every new StockEvent via webhook. Dashboard receives WebSocket message. Mobile polls every 30s or on app focus.

Ops & Runbook

  • If a batch job stays in PROCESSING for more than 5 minutes, alert escalates. Investigate: tail Kafka consumer logs, check correlation IDs in BatchJobItem. Timeout over 10 minutes: manual failure and retry.
  • Nightly cron sums StockEvent entries, compares with current StockSnapshot. Mismatch over 5%: alert to ops team. Resolution: force refresh and ADJUSTMENT event.
  • Cron at 02:00 UTC compares Payment table with Stripe invoices and AbacatePay transactions. Common case: duplicate webhook, idempotency key resolving correctly.
  • Elasticsearch reindex on schema change: delete old index, run bulk reindex from PostgreSQL snapshot, switch alias. Searches use old index during the operation.
  • Kafka consumer lag above 1000 messages: check PostgreSQL query latency, Elasticsearch indexing speed, external lookup timeout. Scale consumer instances if CPU above 80%.

Security & Privacy

  • Queries filtered by seller_id middleware. Vendor A's JWT cannot access vendor B's data. Database constraints and integration tests verify this isolation cannot be bypassed.
  • PCI compliance via Stripe and AbacatePay. Card numbers never seen by the system. Webhook signatures verified with HMAC-SHA256.
  • @UseGuards(JwtAuthGuard, RoleGuard) on all endpoints. 403 returned and access logged in audit for unauthorized attempts.
  • Batch job correlation IDs generated server-side, not sequentially predictable. Error messages sanitized to avoid leaking implementation details.
  • Daily snapshots to S3 with KMS. Point-in-time recovery for last 30 days. Tested monthly in staging environment.
  • Rate limiting: 1000 req/min per IP for public endpoints, 5000/min per authenticated user for private endpoints.

What I'd Improve Next

  • Extract PaymentsModule as an independent service communicating via gRPC or events. Requires per-service database, meaning refactoring the shared Prisma schema.
  • WebSocket subscriptions for inventory instead of 30s polling. Less battery drain on mobile.
  • ML demand forecasting from historical order data. Auto-suggest restocking quantities for vendors.
  • GraphQL layer to replace REST endpoints. More efficient queries on mobile, simpler federation across the three apps.