Mostrador
A commerce platform for small shops: online store, WhatsApp, point of sale and wholesale price lists on one backend. It is one of the seven products of the Vimo suite.

A small shop in Argentina sells through a website, WhatsApp chats and a counter, and each channel keeps its own stock and its own customer list. Mostrador puts all of them on one order, one stock and one customer. Every shop shares the same database, so one shop must never see another shop's data. The design handoff had 22 panel screens, a store, a site and a mobile app.
Three decisions that shaped it.
- 01
Postgres enforces the isolation, not the code
Why. Every business table has a tenant_id and forced row-level security. The API's role owns no table and cannot bypass the policies. Every query runs in a transaction that sets the shop first, so a forgotten filter returns nothing instead of another shop's rows. A test walks the Postgres catalog and fails if a new table has no policy.
Trade-off. RLS alone does not make indexes work, so the tenant still goes in each WHERE. Foreign keys skip the policies, which needed their own test. And COPY does not work on these tables.
- 02
The outbox is the queue
Why. A domain event is written in the same transaction as the change that caused it. There is no case where an order is saved but its event is lost. A worker takes events in batches and groups them by shop, so a hundred price changes become one storefront revalidation. Editing a product in the panel updates the store within a second.
Trade-off. Delivery is at least once, so every consumer has to be idempotent. It is also one more process to run: without it, an edit never reaches the store and it looks like a cache bug.
- 03
Public search is a Rust service
Why. Search with ILIKE took 212 ms p95 on 50,000 products and found nothing after a typo. The Rust service keeps the public catalog in memory and matches by trigrams: 1.7 ms p95, and it finds the product. Search is the one CPU-bound path in the system, and with no garbage collector the slow requests stay close to the median.
Trade-off. A second language in the backend, and memory that grows with every catalog. If the service is down, Caddy sends the same request to the Go API: the store keeps working, slower and without typo tolerance.




Mostrador
A commerce platform for small shops: online store, WhatsApp, point of sale and wholesale price lists on one backend. It is one of the seven products of the Vimo suite.
- Role
- Architecture and full build
- Stack
- Next.js 15, Expo / React Native, TypeScript, Go, Postgres, Rust

A small shop in Argentina sells through a website, WhatsApp chats and a counter, and each channel keeps its own stock and its own customer list. Mostrador puts all of them on one order, one stock and one customer. Every shop shares the same database, so one shop must never see another shop's data. The design handoff had 22 panel screens, a store, a site and a mobile app.
Three decisions that shaped it.
Postgres enforces the isolation, not the code
Why. Every business table has a tenant_id and forced row-level security. The API's role owns no table and cannot bypass the policies. Every query runs in a transaction that sets the shop first, so a forgotten filter returns nothing instead of another shop's rows. A test walks the Postgres catalog and fails if a new table has no policy.
Trade-off. RLS alone does not make indexes work, so the tenant still goes in each WHERE. Foreign keys skip the policies, which needed their own test. And COPY does not work on these tables.
The outbox is the queue
Why. A domain event is written in the same transaction as the change that caused it. There is no case where an order is saved but its event is lost. A worker takes events in batches and groups them by shop, so a hundred price changes become one storefront revalidation. Editing a product in the panel updates the store within a second.
Trade-off. Delivery is at least once, so every consumer has to be idempotent. It is also one more process to run: without it, an edit never reaches the store and it looks like a cache bug.
Public search is a Rust service
Why. Search with ILIKE took 212 ms p95 on 50,000 products and found nothing after a typo. The Rust service keeps the public catalog in memory and matches by trigrams: 1.7 ms p95, and it finds the product. Search is the one CPU-bound path in the system, and with no garbage collector the slow requests stay close to the median.
Trade-off. A second language in the backend, and memory that grows with every catalog. If the service is down, Caddy sends the same request to the Go API: the store keeps working, slower and without typo tolerance.



