Saltar al contenido
Caso · 02 / 15← Todos los trabajos

Mostrador

Una plataforma de comercio para negocios chicos: tienda online, WhatsApp, punto de venta y listas de precio mayoristas sobre un mismo backend. Es uno de los siete productos de la suite Vimo.

Rol
Arquitectura y desarrollo completo
Stack
Next.js 15, Expo / React Native, TypeScript, Go, Postgres, Rust
Bandeja de WhatsApp
Bandeja de WhatsAppEl aislamiento lo garantiza Postgres, no el código
(01) El problema

Un negocio chico de Argentina vende por la web, por chats de WhatsApp y en el mostrador, y cada canal tiene su propio stock y su propia lista de clientes. Mostrador los pone a todos sobre un mismo pedido, un mismo stock y un mismo cliente. Todas las tiendas comparten la base, así que una tienda no puede ver nunca lo de otra. El handoff de diseño traía 22 pantallas de panel, una tienda, un sitio y una app móvil.

Mi rolDiseñé el backend para que el aislamiento y los eventos se cumplan por construcción, y convertí el handoff en ocho apps dentro de un monorepo.
(02) ResultadosContados en el código
42tablas aisladas por tienda con una sola función de RLS en Postgres
248tests de Go que corren contra un Postgres de verdad, no contra mocks
0unidades sobrevendidas con 500 compradores sobre 10 en stock a la vez
(03) Arquitectura

Tres decisiones que lo definieron.

  1. 01

    El aislamiento lo garantiza Postgres, no el código

    Por qué. Cada tabla de negocio tiene tenant_id y RLS forzada. El rol de la API no es dueño de ninguna tabla y no puede saltearse las políticas. Toda consulta corre en una transacción que fija la tienda primero, así que un filtro olvidado devuelve vacío en vez de las filas de otra tienda. Un test recorre el catálogo de Postgres y falla si una tabla nueva no tiene política.

    El costo. La RLS sola no hace que los índices sirvan, así que el tenant igual va en cada WHERE. Las foreign keys no pasan por las políticas, y eso pidió su propio test. Y COPY no funciona sobre estas tablas.

  2. 02

    El outbox es la cola

    Por qué. Un evento de dominio se escribe en la misma transacción que el cambio que lo causó. No existe el caso de un pedido guardado con su evento perdido. Un worker toma los eventos en lotes y los agrupa por tienda, así cien cambios de precio son una sola revalidación de la tienda. Editar un producto en el panel cambia la tienda en el mismo segundo.

    El costo. La entrega es al menos una vez, así que cada consumidor tiene que ser idempotente. Además es un proceso más para levantar: sin él, un cambio nunca llega a la tienda y parece un problema de cache.

  3. 03

    La búsqueda pública es un servicio en Rust

    Por qué. La búsqueda con ILIKE tardaba 212 ms p95 sobre 50.000 productos y no encontraba nada con un error de tipeo. El servicio en Rust tiene el catálogo público en memoria y compara por trigramas: 1,7 ms p95, y encuentra. La búsqueda es el único camino del sistema que es trabajo de CPU, y sin recolector de basura los pedidos lentos quedan cerca de la mediana.

    El costo. Un segundo lenguaje en el backend, y memoria que crece con cada catálogo. Si el servicio se cae, Caddy manda el mismo pedido a la API en Go: la tienda sigue andando, más lenta y sin tolerancia a tipeo.

(04) GaleríaSeguí scrolleando →
Bandeja de WhatsApp
Bandeja de WhatsApp · Chats, links de pago y notas internas en el panel. La ventana de 24 h decide si una respuesta sale gratis o se paga.
Productos
Productos · Un catálogo y un stock para la tienda, WhatsApp y el mostrador. Precio y stock se editan en el lugar.
Editor de la tienda
Editor de la tienda · Las secciones de la portada son datos, no código. Publicar emite un evento que revalida sólo las páginas de esa tienda.
Alta
Alta · El primer paso crea la tienda de verdad, con los textos y el tema de su rubro. Es el único camino que corre sin tenant.
(05) Qué haría después
01Probar los cuatro adaptadores reales (Mercado Pago, WhatsApp Cloud API, Andreani y ARCA) contra sus sandboxes. Pasan el contrato común, pero ninguno habló todavía con su proveedor.
02Pasar a la API las pantallas del panel que todavía leen datos del prototipo, como marketing y canales. La mayoría necesita tablas que todavía no existen.
03Construir las dos apps de Expo que faltan, la de caja y la de reparto, sobre el mismo paquete de tokens nativos.
Próximo proyecto · 03 / 15
Kabina →Presskits, booking y estadísticas de visitas para DJs
Caso · 02 / 15

Mostrador

Una plataforma de comercio para negocios chicos: tienda online, WhatsApp, punto de venta y listas de precio mayoristas sobre un mismo backend. Es uno de los siete productos de la suite Vimo.

Rol
Arquitectura y desarrollo completo
Stack
Next.js 15, Expo / React Native, TypeScript, Go, Postgres, Rust
Mostrador
(01) El problema

Un negocio chico de Argentina vende por la web, por chats de WhatsApp y en el mostrador, y cada canal tiene su propio stock y su propia lista de clientes. Mostrador los pone a todos sobre un mismo pedido, un mismo stock y un mismo cliente. Todas las tiendas comparten la base, así que una tienda no puede ver nunca lo de otra. El handoff de diseño traía 22 pantallas de panel, una tienda, un sitio y una app móvil.

Mi rolDiseñé el backend para que el aislamiento y los eventos se cumplan por construcción, y convertí el handoff en ocho apps dentro de un monorepo.
(02) Resultados
42tablas aisladas por tienda con una sola función de RLS en Postgres
248tests de Go que corren contra un Postgres de verdad, no contra mocks
0unidades sobrevendidas con 500 compradores sobre 10 en stock a la vez
(03) Arquitectura

Tres decisiones que lo definieron.

01

El aislamiento lo garantiza Postgres, no el código

Por qué. Cada tabla de negocio tiene tenant_id y RLS forzada. El rol de la API no es dueño de ninguna tabla y no puede saltearse las políticas. Toda consulta corre en una transacción que fija la tienda primero, así que un filtro olvidado devuelve vacío en vez de las filas de otra tienda. Un test recorre el catálogo de Postgres y falla si una tabla nueva no tiene política.

El costo. La RLS sola no hace que los índices sirvan, así que el tenant igual va en cada WHERE. Las foreign keys no pasan por las políticas, y eso pidió su propio test. Y COPY no funciona sobre estas tablas.

02

El outbox es la cola

Por qué. Un evento de dominio se escribe en la misma transacción que el cambio que lo causó. No existe el caso de un pedido guardado con su evento perdido. Un worker toma los eventos en lotes y los agrupa por tienda, así cien cambios de precio son una sola revalidación de la tienda. Editar un producto en el panel cambia la tienda en el mismo segundo.

El costo. La entrega es al menos una vez, así que cada consumidor tiene que ser idempotente. Además es un proceso más para levantar: sin él, un cambio nunca llega a la tienda y parece un problema de cache.

03

La búsqueda pública es un servicio en Rust

Por qué. La búsqueda con ILIKE tardaba 212 ms p95 sobre 50.000 productos y no encontraba nada con un error de tipeo. El servicio en Rust tiene el catálogo público en memoria y compara por trigramas: 1,7 ms p95, y encuentra. La búsqueda es el único camino del sistema que es trabajo de CPU, y sin recolector de basura los pedidos lentos quedan cerca de la mediana.

El costo. Un segundo lenguaje en el backend, y memoria que crece con cada catálogo. Si el servicio se cae, Caddy manda el mismo pedido a la API en Go: la tienda sigue andando, más lenta y sin tolerancia a tipeo.

(04) Galería
Bandeja de WhatsApp
Bandeja de WhatsApp · Chats, links de pago y notas internas en el panel. La ventana de 24 h decide si una respuesta sale gratis o se paga.
Productos
Productos · Un catálogo y un stock para la tienda, WhatsApp y el mostrador. Precio y stock se editan en el lugar.
Editor de la tienda
Editor de la tienda · Las secciones de la portada son datos, no código. Publicar emite un evento que revalida sólo las páginas de esa tienda.
Alta
Alta · El primer paso crea la tienda de verdad, con los textos y el tema de su rubro. Es el único camino que corre sin tenant.
(05) Qué haría después
01Probar los cuatro adaptadores reales (Mercado Pago, WhatsApp Cloud API, Andreani y ARCA) contra sus sandboxes. Pasan el contrato común, pero ninguno habló todavía con su proveedor.
02Pasar a la API las pantallas del panel que todavía leen datos del prototipo, como marketing y canales. La mayoría necesita tablas que todavía no existen.
03Construir las dos apps de Expo que faltan, la de caja y la de reparto, sobre el mismo paquete de tokens nativos.
Próximo proyectoKabina →
Nahuel SantillánES