Especificación del catálogo premium multi-tenant
Documento de diseño, no de estado. Nada de esto está construido. Define qué debe tener el catálogo de
mueblespara estar al nivel del mejor producto del monorepo, y qué se hereda de Gastro sin reescribir. Creado 2026-08-29. Autoridad sobre M1–M2 de project.md.
1. La tesis
La carta de Gastro ya resolvió el problema difícil: un escaparate público multi-tenant que se ve caro sin que el dueño pueda arruinarlo. El catálogo de muebles es el mismo problema con tres diferencias que sí obligan a diseñar de nuevo: la variante es un SKU real, el producto tiene dimensiones físicas que deciden la compra, y la conversión no es un pedido sino una solicitud de crédito.
Todo lo demás se porta.
2. Lo que se hereda de Gastro sin rediseñar
Estas piezas están probadas en producción. Se copian con adaptación de nombres, no se reinventan.
| Pieza | Dónde vive hoy | Qué resuelve |
|---|---|---|
| Catálogo de marca curado | domains/tenant/theme_catalog.py |
El dueño elige de una lista validada, nunca valores libres (patrón Shopify). 19 fuentes con pareja de cuerpo recomendada + paletas curadas |
ThemeConfig validado |
domains/tenant/schema.py |
brand_color, accent_color, font_heading, font_body, hero_style, card_radius, layout_style. Valor fuera de catálogo se coacciona, no revienta |
| Derivación de paleta con pisos WCAG | public-menu/src/shared/theme/derivePalette.ts |
El guardarraíl que hace imposible un escaparate ilegible: camina el color hacia claro u oscuro hasta alcanzar 12:1 / 7:1 / 4.5:1 |
| Endpoint público Redis-first | domains/public/router.py + service.py |
slug → tenant_id (TTL 300s) y payload (TTL 60s). Hit < 5 ms, miss < 50 ms. Rate limit 60 req/min por IP |
| Caché en idioma base + traducción superpuesta | domains/public/service.py |
Una edición invalida una clave, no una por idioma |
| Resolución de idioma | resolve_locale() |
?lang= → Accept-Language → base. Solo entre los idiomas que el tenant encendió |
| Layouts intercambiables | public-menu/src/widgets/menu-layouts/ |
5 disposiciones que el dueño elige. El mismo dato se ve como rejilla, acordeón o galería |
| Slug con palabras reservadas | core/slugs.py (events) + gastro |
Un tenant no puede quedarse con app, api ni un segmento del enrutado |
ISR + generateMetadata |
public-menu/src/app/[slug]/page.tsx |
revalidate = 60, OG image por tenant |
| Historial de precios | GastroPriceHistory |
Auditoría de cambios de precio. En muebles es obligatorio: hay crédito de por medio |
Regla: cualquier desviación de estas piezas exige ADR. No se "mejora" al portar.
3. La divergencia central: variante ≠ modificador
Gastro modela las opciones como deltas sobre un precio base: un grupo
Salsas con price_delta por opción, sin existencias propias. Es correcto para
comida y es incorrecto para muebles.
En una mueblería, la misma sala en tela beige y en cuero: - no cuesta lo mismo — y no por un delta, sino por precio propio; - no se acaba a la vez — el stock es por combinación; - no se ve igual — necesita su propia foto; - puede no existir — no todas las combinaciones se fabrican.
Modelo correcto: matriz de variantes
Producto "Sala Milano"
├─ OptionType "Tela" → [Lino beige, Chenille gris, Cuero café]
├─ OptionType "Puestos" → [3 puestos, 5 puestos, Esquinera]
└─ Variantes (combinaciones que EXISTEN, no el producto cartesiano)
├─ Lino beige · 3 puestos → SKU, precio, stock 4, fotos, dimensiones
├─ Cuero café · 3 puestos → SKU, precio, stock 0 (sobre pedido 20 días)
└─ Lino beige · Esquinera → SKU, precio, stock 1 (última de exhibición)
Límites duros, recalibrados con la investigación (ver research/como-lo-hacen-los-mejores.md): máximo 3 tipos de opción y 250 variantes por producto.
El 3 es el número del líder del mercado: Shopify subió su tope de variantes de 100 a 2.048 en octubre de 2025 y dejó las opciones en 3. Es lo que impide que el selector se vuelva un formulario. El 250 es nuestro, y es de carga de datos, no técnico: por encima de ~100 variantes la industria coincide en que hace falta edición masiva — de ahí el requisito de importación CSV en M2.
Precio y stock viven en la variante. El producto no tiene precio, solo un
rango derivado (desde $2.890.000).
4. Modelo de datos
Nombres de tabla con prefijo mueb_. Toda tabla lleva tenant_id, política RLS
registrada en core/rls.py, created_at, updated_at y deleted_at.
| Tabla | Papel | Campos que no son obvios |
|---|---|---|
mueb_categories |
Sala, Comedor, Alcoba, Colchones | sort_order, is_active, imagen de portada |
mueb_products |
La ficha comercial | name, description, story (texto largo editorial), tags[], is_featured, is_hero, warranty_months, care_instructions |
mueb_option_types |
Tela, Color, Medida | display_style: swatch | chip | dropdown. Una tela se elige viéndola, no en una lista |
mueb_option_values |
Lino beige, Cuero café | swatch_hex o swatch_image_key — la muestra visual del material |
mueb_variants |
La entidad que se vende | sku, price, compare_at_price (para tachado), stock_qty, availability_mode, lead_time_days, is_display_unit |
mueb_variant_options |
Une variante ↔ valores | tabla puente |
mueb_media |
Galería | variant_id nullable, sort_order, alt_text, lqip, width, height. La variante sin foto propia hereda la del producto |
mueb_dimensions |
Medidas físicas | width_cm, height_cm, depth_cm, weight_kg, boxed_width_cm (lo que pasa por la puerta), seats |
mueb_materials |
Ficha técnica | material_type, value, detail — densidad de espuma, tipo de madera, gramaje de tela |
mueb_collections |
"Sala Milano completa" | Conjunto con precio de paquete. El mueble se vende en juegos |
mueb_price_history |
Auditoría | portado de Gastro, obligatorio: hay crédito de por medio |
Herencia de imagen — por qué la foto por variante no puede ser obligatoria
IKEA genera con CGI el 75% de sus imágenes de producto, y llegó ahí desde el 12% en 2012. La razón es aritmética: una mesa de comedor en 4 acabados × 3 tamaños son 12 sesiones de fotos. Ninguna mueblería PYME las paga, y por eso el mundo renderiza.
No vamos a montar una tubería de CGI — está fuera del alcance del cliente. Lo que sí
se deriva es un requisito de modelo: una variante sin foto propia hereda la del
producto, y la ficha lo indica sin fingir que la foto corresponde a esa tela. El
model_3d_key queda como hueco previsto para el día en que existan renders, sin que
eso cueste una migración.
El error del 28% — la ficha se renderiza desde UN objeto
Baymard mide que el 28% de los sitios tienen fallos graves con las variantes: los datos no se sincronizan entre variaciones y el comprador no logra evaluar el producto. Es el error más caro que este modelo permite cometer.
Al cambiar de variante deben actualizarse a la vez: precio, precio tachado, existencias, plazo de entrega, galería, medidas y SKU.
Regla: la ficha se arma desde un único objeto de variante seleccionada. Está prohibido leer las medidas del producto y el precio de la variante en el mismo render — es la forma exacta en que nace ese bug, y en muebles termina en devolución porque el comprador decidió con las medidas de otra combinación.
availability_mode — tres estados, no un booleano
Gastro usa is_available: bool ("hoy se acabó el ceviche"). En muebles hay tres
realidades comercialmente distintas, y confundirlas cuesta ventas:
| Modo | Qué dice la ficha | Qué implica |
|---|---|---|
in_stock |
"Entrega en 3 días" | stock_qty > 0, sale de bodega |
made_to_order |
"Sobre pedido · 20 días" | stock_qty = 0 pero se vende igual, con lead_time_days |
display_only |
"Última unidad de exhibición" | Una sola, posiblemente con detalle. No se repone |
Un stock_qty = 0 con availability_mode = in_stock es el único caso que oculta
la variante. Los otros dos venden.
5. Contrato de la API pública
Sin autenticación, rate limit 60 req/min por IP, todo Redis-first.
GET /api/v1/public/catalog/{slug}
?lang=es&category=salas&sort=price_asc&page=1
→ tenant (marca + tema), categorías, productos con rango de precio,
primera foto con LQIP, badge de disponibilidad
GET /api/v1/public/catalog/{slug}/product/{product_slug}
→ ficha completa: variantes con precio/stock/fotos, tipos de opción con
muestras, dimensiones, materiales, garantía, simulación de financiación
GET /api/v1/public/catalog/{slug}/collection/{collection_slug}
→ el juego completo con precio de paquete
GET /api/v1/public/theme/catalog
→ fuentes y paletas curadas (idéntico a Gastro)
POST /api/v1/public/catalog/{slug}/lead
→ solicitud del comprador. Idempotency-Key obligatoria
El listado nunca devuelve todas las variantes. Devuelve rango de precio y la foto principal. Las variantes completas solo en la ficha: son el 80% del payload.
6. El sistema de marca multi-tenant
Se porta el de Gastro con una recuración: sus paletas están afinadas a psicología del color gastronómica —acentos cálidos que estimulan el apetito, azul prohibido— y eso no aplica aquí. El mueble se vende con neutros calmados, maderas y un acento sobrio; el azul es perfectamente válido.
Lo que no se toca:
1. El dueño elige de un catálogo curado, jamás un color arbitrario. Es la
diferencia entre un producto premium y un constructor de páginas feo.
2. derivePalette con pisos de contraste 12:1 / 7:1 / 4.5:1. Si el dueño
elige un acento ilegible sobre su fondo, el sistema lo camina hasta que
cumpla. No hay forma de publicar un catálogo que no se pueda leer.
3. Fuente fuera de catálogo se coacciona a la base, no revienta el render.
Añadir para muebles: layout_style con disposiciones propias —editorial
(foto grande, poco texto), grid_dense (mueblería de volumen), showroom
(ambientes completos)— y photo_ratio, porque una sala se fotografía apaisada
y un armario vertical.
7. Rendimiento — el presupuesto es parte del contrato
Se abre desde WhatsApp, en 4G, en un gama media. No es negociable.
| Métrica | Presupuesto | Cómo se logra |
|---|---|---|
| LCP | < 2.0 s en 4G | Foto principal con priority, srcset responsive, AVIF con fallback WEBP |
| CLS | 0 | width/height reales en mueb_media; el hueco existe antes que la imagen |
| Cambio de variante | < 100 ms, sin recarga | Precarga de las fotos de variantes adyacentes al montar |
| Respuesta de API | < 5 ms cacheada | Redis-first idéntico a Gastro |
| Peso de la ficha | < 250 KB inicial | LQIP en base64 para el blur, galería diferida |
LQIP obligatorio. Un catálogo de muebles es fotografía; un salto de imagen gris a foto se ve barato. Se genera al subir, junto al WEBP, y viaja en el JSON.
8. SEO y datos estructurados — aquí sí importa
Una carta de restaurante casi no se busca en Google. Un sofá sí. Esta es la diferencia de prioridad más grande frente a Gastro.
- JSON-LD
Product+Offerpor variante, conpriceCurrency: "COP",availabilityeitemCondition. Google pidename,imagey unOfferbien formado para el fragmento de producto; para merchant listing completo hacen falta ademásbrand, un identificador de producto,aggregateRatingyshippingDetails— y sonshippingRate+deliveryTimelos que disparan la anotación de fecha de entrega. - La ficha técnica es lo que hace citable el producto por una IA. Los asistentes de
compra leen las propiedades detalladas —materiales, medidas— para responder preguntas
concretas.
mueb_materialsymueb_dimensionsno son solo para el humano. sitemap.xmlpor tenant, generado desde el catálogo activo.- URL canónica por producto:
/{slug}/{categoria}/{producto}. La variante va en query (?tela=lino-beige) para no fragmentar la autoridad de la página. - OG image por producto, no solo por tenant: lo que se ve al pegar el enlace en WhatsApp es la miniatura del producto.
9. El gesto central: el enlace de WhatsApp
Es la razón de existir de la vertical y Gastro no tiene equivalente.
Cada producto y cada variante genera un enlace con mensaje prellenado:
https://wa.me/57XXXXXXXXXX?text=Hola,%20me%20interesa%20la%20Sala%20Milano
%20en%20lino%20beige%20(3%20puestos)%20—%20muebles.timeliber.com.co/lacasa/sala-milano?tela=lino-beige
Requisitos: - Botón "Compartir" en la ficha, con la variante seleccionada ya incluida. - El panel muestra el enlace listo para copiar de cada producto. - Atribución: el enlace lleva parámetro de origen para poder medir la métrica de éxito de mission.md — cuántas fichas abiertas desde WhatsApp terminan en solicitud.
10. Lo específico de muebles que casi nadie hace
Estas cinco cosas son la diferencia entre un e-commerce genérico y una herramienta que un mueblero colombiano reconoce como hecha para él.
- "¿Cabe?" — no es un diferencial, es EL pilar. La investigación del sector lo
pone como la pregunta más común del comercio de muebles en línea, y la ansiedad de
escala explica una parte importante de las devoluciones. Tres piezas:
boxed_width_cmcon advertencia al superar los 80 cm de puerta habitual; medidas que cambian con la variante seleccionada; y comparación con objetos familiares, porque traducir «180 cm» a un número no basta — hay que anclarlo a algo que el comprador reconozca. - La cuota antes de preguntar. El bloque de financiación en la ficha, con cifras de la pasarela, nunca calculadas en el frontend. Es la mitad de la propuesta de valor y depende de [MUE-005].
- Muestra de material visible y GRANDE. Elegir "Chenille gris" en un desplegable es inútil: se elige viendo la tela. Sustituir desplegables por muestras visuales está entre las pruebas A/B más consistentes de todo el comercio electrónico. Y la lección del mueble a medida es que la muestra se vea grande, no como un círculo de 20 px.
- Ficha técnica real — densidad de espuma, tipo de madera, gramaje. El comprador informado de muebles pregunta esto, y quien responde, vende.
- Comparador de 2–3 productos. Nadie compra el primer sofá que ve. Se
reaprovecha la mecánica de
features/shortlistque Gastro ya construyó para el Modo Mesero.
11. Accesibilidad
WCAG 2.2 AA es el mínimo del estándar de frontend del monorepo y hay gate en CI
(a11y-gate.yml, reutilizable, ya lo invocan cinco frontends).
Lo que rompe específicamente en un catálogo de muebles:
- Las muestras de color necesitan etiqueta de texto: "Lino beige" no puede
comunicarse solo por el color del círculo.
- El estado seleccionado de una variante necesita aria-pressed, no solo un borde.
- El zoom de foto debe funcionar con teclado, no solo con pinch.
- Los precios con font-variant-numeric: tabular-nums para que la comparación
alinee.
12. Líneas rojas
- Nunca una cuota de financiación calculada en el cliente.
- Nunca precio en el producto: vive en la variante.
- Nunca el producto cartesiano de opciones como variantes.
- Nunca un color de marca sin pasar por
derivePalette. - Nunca una foto sin
width/heightreales — es CLS garantizado. - Nunca devolver todas las variantes en el listado.
- Nunca ocultar una variante
made_to_orderpor tener stock 0: se vende.
13. Referencia de conversión
La conversión media de muebles y hogar en 2025 está entre 1.2% y 1.6%. Es el listón contra el cual medir, y ordena la expectativa: la métrica de éxito de la misión no son visitas al catálogo, es qué fracción de las fichas abiertas desde WhatsApp termina en solicitud.
Contexto útil: Baymard mide que el 52% de las fichas de producto en escritorio y el 62% en móvil tienen un desempeño de UX «mediocre o peor». El listón real del mercado está bajo, y eso es precisamente la oportunidad.
Investigación completa con fuentes en research/como-lo-hacen-los-mejores.md.