Skip to content
All projects
Freelance
Freelance

b2b E-Commerce for a local distributor

A B2B ordering platform for a pharmacy-products distributor: a NestJS API, a React portal and admin dashboard, a Flutter salesperson app, and real-time order updates through a Go WebSocket service.

b2b E-Commerce for a local distributor

Distribuidora Luna is a pharmacy products distributor based in Uruguay that serves a large network of pharmacies. As the business grew, keeping up with purchase orders and coordinating logistics by phone and on paper stopped scaling.

I designed and built the whole platform. Pharmacies order through a dedicated web portal with managed accounts; every new order appears instantly, with a notification sound, on the admin dashboard. The company's on-street salesperson uses a mobile app to place orders on behalf of the pharmacies they visit and is notified of new orders, which prevents duplicate visits to pharmacies that already ordered that day.

Architecture

A modular NestJS API (Node.js, TypeScript, Sequelize) owns the business logic and a PostgreSQL database. One React and Mantine web app serves both the pharmacy portal and the admin dashboard, and the salesperson app is built with Flutter. Real-time updates are handled by a separate Go service: the real-time UI update component I later published as part of OptiBlocks. Everything runs in Docker on a Hostinger VPS behind nginx.

UML deployment diagram: pharmacy PC, office PC and mobile device connect over HTTPS to nginx on a Hostinger VPS running Docker; nginx proxies to the NestJS API and the Go real-time UI update service; the API uses PostgreSQL 15 and an uploads volume, and posts each committed order to the Go service.
Deployment view (UML 2 deployment diagram). Click to open at full resolution.

Real-time order updates

  • The API never holds a socket: after an order commits, the API sends one POST to the Go service, which fans it out to every connected dashboard over WebSockets. Node's event loop stays free for API requests.
  • Publish after commit: the push is fire-and-forget with a 5-second timeout. A dashboard never shows an order that rolled back, and a problem in the real-time path can never fail an order.
  • Two kinds of credentials: dashboards subscribe with the same JWT they use for the API. The backend publishes with a time token, an HMAC-SHA256 of the current time window, so the shared secret never travels and a leaked token expires with its window.
  • Slow clients don't slow everyone: one hub goroutine owns the set of clients, and every connection has its own buffered send queue. A client whose queue stays full for 5 seconds is disconnected, pings every 30 seconds drop dead connections, and the dashboard reconnects with exponential backoff.
UML sequence diagram PlaceOrder: the controller's JWT guard, the pharmacy lookup, pricing in one SQL query, a transaction that inserts the order and one line per product with its promotion, then an asynchronous sendOrder to the Go service, which validates the time token and broadcasts the order to the admin dashboard over a WebSocket.
Placing an order (UML sequence diagram).

Placing an order

  • Scoped access: administrators need the orders role, and a pharmacy user can only order for, view and download invoices of the pharmacies linked to their account.
  • Pricing in the database: the cart is sent to PostgreSQL as a VALUES list and joined against the products in one query, instead of loading each product.
  • Promotions applied atomically: each line gets the best active “buy N get M” promotion (the largest N that does not exceed the quantity) and is written in the same transaction as the order.

Data model and performance

Administrators, salespeople and pharmacy users share one user table with a role-specific extension each; administrators carry a list of permissions (stats, orders, products, promotions, users). Pharmacy users and pharmacies are many-to-many, so one person can order for several branches.

Data model: User with Admin, Vendedor and Cliente extensions; Cliente and Farmacia linked many to many; Farmacia has many Pedidos; Pedido has many PedidoProducto lines referencing Producto and an optional Promocion; Producto belongs to a Distribuidora and a Categoria.
Logical data model in crow's-foot notation, from the Sequelize migrations.
  • Indexes for an append-only orders table: a partial index on pending orders, a partial composite index for each pharmacy's recent orders, and a BRIN index on the creation date.
  • Dashboard KPIs: revenue, orders, active and new pharmacies, average order value, sales by category and pharmacy, top products and the sales trend come from nine aggregate SQL queries run in parallel.
  • Catalog import: the distributor's Excel price list is uploaded as is; products are upserted and new categories created in one transaction.
  • Accounts: bcrypt-hashed passwords, 15-minute access tokens and 30-day refresh tokens.

Key features

  • Pharmacy portal: product browsing (categories, search, sorting, promotions), cart, order creation, order history and invoices.
  • Admin dashboard: KPIs and analytics, plus live order management: pending and completed views, search, pagination and status updates.
  • Invoices: admins attach a PDF invoice when completing an order; authorized users can list and download them.
  • User and access management: administrators with role-based permissions, plus salesperson, pharmacy user and pharmacy management, including enabling or disabling pharmacies.
  • Promotions: “buy N get M” promotions tied to products, with activation toggles.
  • Salesperson mobile app: authentication, fast product search, cart-based order creation on behalf of a selected pharmacy, and active orders.

Gallery