00 — Aviso al lector
Este documento es lectura honesta del estado de bodados.com a fecha de julio de 2026. Cubre las decisiones de design system y producto tomadas desde el primer commit (18 febrero 2026) hasta el cierre de Phase 1. No es caso de éxito. Es el inventario de decisiones que sostiene una plataforma editorial wedding-tech con doble audiencia (pareja consumer + vendor SaaS B2B) operada por un fundador único con disciplina de Design System Architect.
La pieza es distinta a los otros casos de la serie. Aquí los agentes IA no son una capa lateral sobre un producto editorial. Son superficie del producto: tocan datos privados del usuario autenticado y modifican estado. Si en lavigencia los agentes producían contenido público bajo curaduría humana del conjunto, en bodados los agentes operan sobre el evento real de cada pareja. La pareja le dice al asistente “marca a García como confirmado” y eso modifica la fila real en la tabla guests. Esta diferencia obliga a redefinir qué es un agente dentro de un producto consumer y exige decisiones de DS que pocos casos públicos hispanohablantes han documentado.
Sin AI-slop. Sin “elevar”, sin “transformar”. Quien necesite ese lenguaje no es el público de este documento.
01 — La decisión que cambia todo
El sector nupcial español mueve alrededor de 150.000 bodas al año. Dos directorios B2B legacy (Bodas.net, Joy) dominan la atención de los vendors profesionales cobrando fee fijo por aparecer en listado, sin ROI medible para el vendor. La parte B2C de planificación lleva décadas atascada en Excel + WhatsApp. Los “wedding planner SaaS” americanos importan modelo y voz que no funcionan en mercado español.
La decisión fundacional de bodados es rechazar el modelo de directorio que paga el vendor por aparecer, y construir el modelo opuesto: la pareja paga 49 € pago único por organizar su boda con disciplina editorial, y el vendor profesional cobra comisión por transacción cuando un invitado regala vía la mesa curada que el vendor ofrece.
Cita literal del producto: “Bodas.net te cobra. Bodados te paga.”
Esta decisión obliga a reescribir cuatro preguntas básicas que un equipo de design system suele dar por sentadas:
- ¿Qué es el producto cuando la pareja organiza solo una boda en su vida? Pago único, no suscripción. El SaaS mensual es ridículo cuando el evento tiene fecha fija.
- ¿Qué es la audiencia primaria cuando hay dos lados de mercado? Dos superficies con dos URL públicas (bodados.com para consumer, pro.bodados.com para vendor) sobre el mismo worker.
- ¿Qué es el componente “asistente IA” cuando el dashboard contiene los datos íntegros de un evento privado? Una superficie con acción real autenticada, no un chatbot lateral.
- ¿Qué es la voz cuando el producto compite contra un sector saturado de “boda de tus sueños”? Vocabulario prohibido explícito en el sistema, no en un manual.
bodados está construido respondiendo a estas cuatro preguntas con disciplina. El resto del documento explica cómo.
02 — Pago único como decisión arquitectónica, no de pricing
La decisión más subrayable del proyecto desde el punto de vista de un Design System Architect es el rechazo explícito de la suscripción mensual.
La razón operativa
La competencia entera del sector cobra anual o mensual. bodados elige pago único 49 € (44 € con código pioneras, después 79 €) aunque la matemática LTV diga lo contrario. La razón es operativa, no estética: la pareja organiza una sola boda. Un SaaS mensual es ridículo cuando el evento tiene fecha fija.
Lo que se desbloquea
Esta decisión simplifica todo el sistema:
- Sin billing recurring page. No hay flujo de renovación, no hay dunning, no hay churn analysis. Un pago, hecho.
- Sin downgrade flow. No hay paths “qué pasa si bajo a Esencial”. La pareja paga una vez y mantiene Pro para siempre sobre ese evento.
- Sin pricing experiments mensuales. El precio es fijo. El experimento se hace en otra dimensión (fase pioneras a 44 €, después 79 €), no en cuotas.
- Sin lock-in psicológico. La copy del producto declara “nada de letra pequeña. Sin cuota que cancelar”. Esto es contrato explícito con el usuario.
La voz se alinea con la decisión
La copy de /precios está construida sobre esta decisión:
- “Precios honestos. Nada de letra pequeña.”
- “Esencial 0€ · Pro 49€ pago único”
Para un Design System Architect senior la lectura es directa: cuando una decisión de modelo de negocio elimina superficies enteras del producto, esa decisión es arquitectónica antes que comercial. Cada “no” del modelo se traduce en componentes que no se construyen.
03 — Routing per-subdomain en un solo worker
Esta es la pieza arquitectónica más interesante para un DS lead que opera productos multi-tenant.
El problema
bodados tiene dos audiencias con disposición a pagar y vocabulario muy distintos:
- Pareja consumer en
bodados.com: vocabulario íntimo, plural “vosotros”, invitación editorial, RSVP, presupuesto, mesa de regalos, agendas. Compra pago único 49 €. - Vendor profesional B2B en
pro.bodados.com: vocabulario operativo, calculadora de LTV, curaduría de catálogo, Stripe Connect, comisión por transacción. Cobra 2,9% + 0,50 € por gift transaccional.
Mostrar los dos pricings en la misma landing rompe a ambos. La pareja descarta “2,9% + 0,50€” como incomprensible. El vendor descarta “49 € pago único” como amateur (es su cliente, no él).
La solución arquitectónica
Middleware Next.js que lee host header y reescribe rutas sin cambiar la URL pública:
bodados.com/→ renderiza la home consumerpro.bodados.com/→ reescribe a/proy renderiza vendor landingpro.bodados.com/{path}→ reescribe a/vendor/{path}preservando subdomain en el bar de direccionespro.bodados.com/api/*y/_next/*pasan through sin reescritura
Single worker. Single bundle. Single deploy. Cero cold-start adicional. El bundle de JavaScript no se duplica. La caché de assets es compartida. Los KV namespaces son los mismos.
Para un DS senior la lectura es la misma que en lorsclub con su routing por subdominio: una arquitectura técnica al servicio de una arquitectura comercial. La diferencia es que aquí lo hace un middleware estándar de Next.js, no un Worker dispatcher dedicado. Patrón más simple, mismo resultado.
Lo que esto exige al design system
Los tokens son compartidos entre las dos audiencias, pero los componentes se especializan por surface:
<LandingPricingSection />tiene dos modos: consumer (tres tiers verticales con pago único destacado) y vendor (calculadora interactiva + comisión visible).<NewsletterSignup />cambia copy según contexto. “Cero ads. Una vez por semana. Tres minutos.” en vendor; copy distinto en consumer.<Hero />recibeaudience: 'couple' | 'vendor'y elige tipografía, ritmo, paleta.
Sin tokens compartidos, esto sería dos productos. Con tokens compartidos, es un producto con dos puertas.
04 — Cinco agentes IA con acción real sobre datos privados
Aquí está la decisión más distintiva del proyecto y la que merece más atención del documento.
Qué no es
A diferencia del 99% de productos consumer con IA, los agentes de bodados no son chatbots laterales. No hay un widget azul abajo a la derecha que responde “hola, ¿en qué te ayudo?”. No hay autocompletar de prompts en un input genérico. No hay sugerencias de marketing disfrazadas de asistente.
Qué sí es
Cinco agentes embebidos en superficies específicas del dashboard, cada uno con acción real autenticada sobre datos privados del owner:
| Agente | Superficie | Acción real que puede ejecutar |
|---|---|---|
guest-agent | /dashboard/invitados | Añade, marca confirmado, borra, edita campos en la tabla guests |
seating-agent | /dashboard/seating | Asigna invitados a mesas, valida restricciones (alergias, parejas, conflictos) |
invitation-agent | /dashboard/diseno/editor | Modifica campos de invitation_configs: tagline, paleta, copy de bloques |
fashion-agent | /dashboard/looks | Aconseja outfits con contexto del evento (estación, dress code, locación) |
legal-agent | /dashboard/legal | Responde preguntas legales con RAG sobre KB legal pgvector con citas trazables |
La pareja le escribe al asistente “marca a García como confirmado” y el agente modifica la fila real en la tabla. No es sugerencia. Es acción ejecutada.
Por qué esto es decisión de DS
Tres consecuencias para el sistema que un DS senior reconoce inmediatamente.
Componentes con superficie agéntica + estado real. El componente que renderiza la lista de invitados (<GuestList />) tiene que reaccionar a cambios producidos por dos fuentes: edición manual del usuario y acciones del agente. Esto exige optimistic UI con reconciliación, no solo refresh tras acción. Es decisión arquitectónica del DS, no del agente.
Auth como token primario, no como decoración. Cada llamada a agente lleva supabase.auth.getUser() server-side. El agente no puede modificar invitados de otra pareja aunque se lo pidan. Esto se codifica en agentGuard() (rate-limit + auth + ownership check). Sin esa capa, los agentes son vector de ataque trivial.
Vocabulario de acción acotado. El system prompt de cada agente declara qué acciones puede ejecutar como herramientas estructuradas (tool use). El guest-agent no puede modificar invitation_config. El invitation-agent no puede borrar invitados. Las acciones están enumeradas, no abiertas.
Telemetría agéntica
Cada agente loguea con [agent-telemetry] prefix estructurado que Cloudflare Workers Logs puede parsear sin tabla específica en DB. Esto es decisión deliberada para Phase 1: una migration SQL para agent_usage_log se justifica cuando hay volumen para mantenerla. Mientras tanto, console.log con prefix sirve.
La distinción que merece subrayar
bodados ilustra una distinción que en la serie de Casos Vivos aparece por primera vez aquí: agente con acción real autenticada sobre estado privado, distinto a:
- Agente externo que consume contenido público (lorsclub).
- Agente embebido que consulta sobre contexto (Lo Peix concierge).
- Agente como redacción que produce contenido para curación humana del conjunto (La Vigencia).
Las cuatro variantes son legítimas y exigen disciplinas de DS distintas. Confundirlas es ruta directa a la sobreingeniería o al subdiseño.
05 — Pipeline editorial agéntico con critic-loop Reflexion
El blog de bodados se publica 4 veces al día sin intervención humana, con calidad medible. La arquitectura que lo permite es lo que un DS lead que opera contenido editorial con IA debe conocer.
El problema
Generar 4 posts al día con LLM directo es trivial. Generar 4 posts al día que cumplan contrato editorial publicable es muy distinto. La mayoría de productos editoriales con IA fallan en este segundo paso. Publican prosa LLM con sus vicios: introducciones genéricas, falta de evidencia, dataset de modelos visible en el output.
La solución: tres pasos encadenados
Paso 1 · Generación con prompt editorial. DeepSeek-V3.1 vía Together AI con system prompt extenso (src/app/api/cron/blog-generate-scheduled/route.ts) que define:
- Persona explícita: “Alejandra, redactora estrella de Bodados. Ex-Vogue Weddings, 6 años. Tu prosa mezcla glamour parisino con calidez mediterránea.”
- 9 reglas editoriales operativas: primera frase = dato concreto (precio, %, provocación), nunca genérica. Direct-answer block de 40-60 palabras tras el primer H2. Mínimo dos detalles sensoriales por post. H2 editoriales, no SEO-spam. Mínimo 1.500 palabras.
- Vocabulario prohibido explícito: “boda de tus sueños”, “cuento de hadas”, “mágico”, “el día más importante”, “experiencia única”, “espectacular”, “encantador”, “pintoresco”, “paraíso”, “un lugar con encanto”, “ideal para parejas”.
Output forzado en JSON mode con response_format: json_object. Campos obligatorios: {slug, title, seoTitle, seoDescription, excerpt, body, evidence[]}.
Paso 2 · Critic-loop Reflexion. El mismo modelo evalúa el output del paso 1 sobre rúbrica de cinco dimensiones:
- Factualidad (las evidencias son verificables).
- Estructura (direct-answer block presente, H2 editoriales, párrafos balanceados).
- Voz y tono (alineado con persona Alejandra, sin vocabulario prohibido).
- SEO técnico (longitudes correctas, slug válido, OG metadata).
- Higiene de lenguaje (sin “as an AI”, sin
{{var}}sin rellenar, sin nombres de modelos).
Si el score agregado es menor que 7, el sistema revisa una vez. Cap de una revisión: el paper original de Reflexion documenta diminishing returns después de la primera. No se itera infinitamente.
Paso 3 · Hard validator. validateGeneratedPost() ejecuta verificaciones binarias que no dependen del LLM:
- Wordcount entre 500 y 2.500.
- Número de H2 entre 1 y 8.
- Ratio de stopwords español ≥ 4% (detecta texto no español).
- Regex anti-LLM-artifacts: bloquea “as an AI”, “{{var}}” sin rellenar, nombres explícitos de modelos.
Solo si score ≥ umbral configurable AND validator.ok → upsert a blog_posts. Si cualquier paso falla, el post se rechaza y el sistema escoge otro topic del queue. El humano no edita prosa LLM. El contrato es binario: pasa o no pasa.
Por qué esto es DS y no contenido
Es el equivalente, en lenguaje de diseño, a tener tokens en el código fuente + un linter que verifica que se usan correctamente + tests visuales que comparan output contra snapshots aprobados. Ningún DS senior confía solo en que “los diseñadores usarán los tokens bien”. El sistema obliga.
bodados aplica la misma disciplina al output editorial. La persona Alejandra es un token de voz. La rúbrica de cinco dimensiones es un linter. El hard validator es testing visual.
Lo que distingue esto de un blog LLM genérico
Tres cosas:
evidence[]obligatorio. El validator rechaza posts sin evidencias estructuradas. La prosa con datos concretos preserva la diferenciación frente a sintetizadores.- Cap de una revisión. Decisión arquitectónica, no técnica. Iteraciones múltiples introducen drift, no calidad.
- Cero edición manual. El owner no toca prosa. Si el post no pasa, se descarta y se intenta otro topic. La calidad emerge del sistema, no de la curaduría per-pieza.
06 — Atomic claim antes de Stripe checkout
Patrón replicable que merece subrayar para cualquier DS que opere comercio con recursos compartidos (slots, plazas, asientos, items únicos).
El problema
La mesa de regalos del vendor tiene 8-12 items simbólicos. Dos invitados pueden hacer click en “regalar” sobre el mismo item al mismo tiempo. Si el sistema lanza dos Stripe checkout sessions y ambos pagan, el vendor se queda con dos cobros por un item que no puede entregar dos veces.
La solución
Antes de crear la sesión de Stripe, ejecutar un UPDATE atómico:
UPDATE gifts
SET status = 'reserved', reserved_until = NOW() + INTERVAL '15 minutes'
WHERE id = $1 AND status = 'available'
RETURNING id
Tres outcomes posibles:
- Retorna 1 row → el invitado ganó la carrera. Procede con Stripe checkout. La reserva queda viva 15 minutos.
- Retorna 0 rows → otro invitado fue antes. Responder 409 sin tocar Stripe. UI muestra mensaje claro.
- Stripe checkout falla → revert:
UPDATE gifts SET status = 'available' WHERE id = $1 AND status = 'reserved'.
Si la sesión de Stripe expira sin pago, un job verifica reservations vencidas y revierte. Si el pago llega, el webhook marca status = 'paid'.
Por qué esto es decisión arquitectónica
Tres consecuencias.
Sin SELECT FOR UPDATE. El patrón funciona sin lock explícito. Postgres garantiza atomicidad del UPDATE WHERE. Es más simple, más legible, y no introduce locks que pueden bloquear otras transacciones.
Idempotency key por gift. Stripe ya tiene su propio mecanismo de idempotency. Combinado con el atomic claim, doble protección: race resuelta antes de tocar Stripe, retry seguro si la sesión se interrumpe.
Replicable a cualquier recurso único. Plazas de un curso, asientos numerados, items de stock limitado, slots de calendario. La forma del patrón es la misma: UPDATE WHERE estado_disponible RETURNING.
Para un DS senior la lectura: cuando tu sistema asigna recursos únicos a usuarios concurrentes, el atomic claim antes del side effect externo es el patrón mínimo viable. Sin esto, las race conditions son cuestión de tiempo, no de probabilidad.
07 — Catálogo curado en TypeScript como source of truth
Decisión arquitectónica pequeña que un DS senior aprecia porque la practica con tokens.
El patrón
bodados tiene tres catálogos críticos:
- 28 templates de invitación en
templateCatalog.ts. - 8 verticales de vendor en
pro-roles.ts(fotógrafos, planners, floristas, catering, DJs, videógrafos, maquilladoras, venues). - FAQ items en
faq-items.ts.
Los tres viven como const exportadas en TypeScript. No están en base de datos. No tienen admin UI.
Por qué no en DB
Tres razones.
Cambios versionables en git. Cuando se modifica un template, el commit deja registro auditable. Cuando se añade un vertical, el PR contiene el contexto editorial. Sin esto, los cambios viven en una tabla que nadie revisa hasta que algo se rompe.
Sin migration ni admin UI. No hay schema para “templates de invitación con sus 12 campos editables”. No hay UI para que el editor lo modifique. Esto desbloquea velocidad de cambio: el owner edita el TypeScript, hace deploy, listo.
Type safety automática. El consumidor del catálogo (componentes que renderizan templates, páginas que iteran verticales) tiene autocompletado y errores en compile time. Si alguien rompe el shape de un template, el build falla antes de llegar a producción.
Cuándo migrar a DB
La regla operativa: se migra a DB cuando un editor humano necesita modificarlo sin deploy. Mientras el catálogo lo edita el owner, vive en TypeScript. El día que entre un content manager que no quiere tocar código, ese día se justifica admin UI + migration.
Para un DS senior la lectura: el catálogo en código es la versión madura de los design tokens. Mismo patrón, distinto dominio. Lo que diferencia al sistema bien diseñado del sistema sobreingeniado es no construir admin UIs hasta que existe la persona que las va a usar.
08 — Dual-write con env-driven mode resolution
Patrón de migración aplicado durante el cambio de Supabase a Neon en abril de 2026. Replicable a cualquier migración de DB sin big-bang.
El problema
Migrar bases de datos en productos en producción tiene dos modos clásicos: big-bang (corte total, riesgo de rollback complejo) o feature flag por escritura (cada feature decide manualmente). Ambos son frágiles.
La solución
Variable de entorno por tabla controlando el modo de escritura:
MIGRATION_BLOG_POSTS_WRITE_MODE = supabase | neon | dual
Default dual durante el watch period. Cada upsert intenta ambas DBs:
- Supabase queda como canonical (failure aborta la operación).
- Neon queda como mirror (failure se loguea pero no aborta).
Cuando las dos DBs llevan días sin divergencias, se cambia el env var a neon: Neon pasa a canonical, Supabase queda como fallback. Cuando el período de seguridad termina, se elimina el dual-write y Supabase se retira.
Lo que esto desbloquea
Tres ventajas operativas.
Rollback es un env var. Si Neon empieza a fallar en producción, el owner cambia dual por supabase y el sistema vuelve a confiar solo en Supabase. Sin redeploy. Sin migración inversa.
Verificación continua. Durante el watch period, queries que comparan checksums de ambas tablas detectan divergencias antes de promover Neon a canonical.
Cutover sin downtime. El cambio de canonical es un env var, no una operación de DB. La aplicación sigue funcionando durante el switch.
Aplicado a 5 tablas
Durante la migración, dual-write se aplicó a feature_flags, blog_posts, market_intelligence, blog_generation_queue y onboarding_email_steps. Las tablas auth-coupled (events, guests, invitations) permanecen en Supabase hasta que la migración de auth a Clerk se complete.
Para un DS senior la lectura: cuando un sistema migra infraestructura crítica, el coste de mantener dual-write durante semanas es menor que el coste de un rollback con datos divergentes. La verificación continua durante el watch period vale el doble de escrituras.
09 — Tokens y design system visual
bodados tiene design system parcialmente formalizado: tokens CSS canónicos, sin Storybook publicado, componentes formalizados sólo para invitations + dashboard.
Tokens cromáticos semánticos
Paleta editorial cálida con tres familias funcionales:
--paper,--ivory,--ivory-softpara fondos.--ink,--ink-mute,--ink-faintpara texto en tres niveles.--forest,--forest-deepcomo acento secundario.--gold,--gold-softcomo accent premium para CTAs.--hairline,--hairline-softpara líneas estructurales.
El color no se usa por nombre familiar (no hay --green-500). Cada token codifica intención. --ink es texto primario, --ink-mute es secundario, --gold es acción premium. Esta es la versión madura de los design tokens: el color como significado, no como variante de paleta.
Cuatro familias tipográficas declaradas
--font-editorial-serif(Newsreader) → narrative copy, headlines, pull quotes.--font-editorial-sans(Inter Tight) → UI labels y números.--font-editorial-script(cursive) → italics decorativos puntuales.--font-editorial-data(Inter Tight) → tabular numbers en presupuesto.--font-dashboard-sans→ separada para no contaminar invitación editorial con la tipografía operativa del dashboard.
Cuatro familias es decisión inusual y editorial. Posiciona el producto fuera del estándar SaaS (Inter solo) y dentro del estándar editorial (Newsreader principal). La separación entre tipografía de invitación y tipografía de dashboard es decisión crítica: la pareja experimenta dos productos, no uno.
28 templates con perfiles de movimiento y layout
Cada template lleva dos perfiles declarados:
motionProfile: cinematic / editorial / minimal. Controla las variants de framer-motion (timing, easing, stagger).layoutProfile: immersive / story / balanced. Controla la jerarquía visual (full-bleed hero / two-column / centered).
La combinación genera 9 posibles modos visuales sobre los mismos componentes base. Esto es DS bien resuelto: la variabilidad vive en los perfiles, no en componentes duplicados.
Print stylesheet completo
Detalle pequeño que un DS senior aprecia. La invitación tiene @media print completo en globals.css. La pareja puede imprimir su invitación digital y obtener un documento físico legible y editorial. No es feature secundaria: para muchos invitados mayores la invitación impresa sigue siendo la principal.
Animaciones acotadas
Dos animaciones nombradas:
countdown-pulse: 350ms ease-out por tick del countdown.vpMarquee: 60s loop para value-prop pills.
Ambas con prefers-reduced-motion respetado. El producto se anima por decisión editorial, no por defecto.
10 — Indexación agéntica y defensa contra scrapers
bodados expone schemas y endpoints para agentes editoriales legítimos y bloquea explícitamente scrapers que extraerían contenido para entrenamiento sin valor de vuelta.
27 schemas JSON-LD
Desde Organization, WebSite, BreadcrumbList, Article, BlogPosting, FAQPage, Product, Offer, SoftwareApplication, Person, LocalBusiness, Event, Place, PostalAddress, hasta AggregateRating, Review, Service, OfferCatalog, ContactPoint, WebApplication, ItemList, Question, Answer.
La densidad permite que cada superficie del producto sea legible por agentes con la representación correcta: el blog es Article, la home es WebSite con SearchAction, el vendor landing es LocalBusiness, cada precio es Offer.
Defensa edge en Cloudflare
Tres capas activas:
ai_bots_protection: "block". Bloquea GPTBot, Claude-Web, CCBot, Google-Extended antes de que toquen el worker. Decisión deliberada: el contenido editorial de bodados es activo de marca, no training data gratuita.crawler_protection: "enabled". Verifica que Googlebot/Bingbot reales (no spoofeados) por reverse DNS.fight_mode: true+enable_js: true. JS challenge para bots simples que no ejecutan JavaScript.
Por qué bloquear scrapers pero exponer schemas
La distinción que muchos productos no hacen: agentes legítimos respetan robots/headers, scrapers de training no. Los schemas JSON-LD sirven a Google AI Overviews, Perplexity con consentimiento, asistentes que respetan políticas. Los scrapers que ignoran headers se bloquean antes de tocar el contenido.
Para un DS senior la lectura: defender contra scrapers no es lo mismo que ocultarse de agentes. Bodados expone schemas densos al canal correcto y cierra el canal incorrecto.
11 — Voz editorial codificada en sistema
bodados tiene voz editorial pautada. No vive solo en el cerebro del owner, vive en el sistema en dos capas.
Capa 1: vocabulario prohibido en el prompt del blog
El system prompt del cron que genera el blog declara explícitamente las frases que nunca deben aparecer en una pieza:
- “boda de tus sueños”
- “cuento de hadas”
- “mágico”
- “el día más importante”
- “experiencia única”
- “espectacular”
- “encantador”
- “pintoresco”
- “paraíso”
- “un lugar con encanto”
- “ideal para parejas”
Esto es el AI-slop del sector nupcial. Aparece en cada blog de cada competidor. Su ausencia es activo de diferenciación.
Capa 2: persona específica como token de voz
“Alejandra, redactora estrella de Bodados. Ex-Vogue Weddings, 6 años. Tu prosa mezcla glamour parisino con calidez mediterránea.”
La persona es token. El LLM produce prosa alineada con esa persona aunque el operador no revise cada pieza. Cuando la persona evoluciona, todo el corpus mejora retroactivamente.
Frases citables del producto vivo
Doce frases que aparecen en producción y funcionan como vocabulario de marca:
- “Precios honestos. Nada de letra pequeña.” — posiciona contra suscripciones SaaS opacas.
- “Bodas.net te cobra. Bodados te paga.” — posicionamiento adversarial directo.
- “Una boda media son 975 € extra al año.” — promesa cuantitativa para vendor.
- “Tres pasos. Cero fricción.” — pitch del flujo vendor.
- “Pinchas la fiesta, cobras los regalos.” — voz coloquial específica para
/pro/djs. - “Empieza el día con ella, termina el día contigo.” — voz íntima para
/pro/maquilladoras. - “Cero ads. Una vez por semana. Tres minutos.” — promesa anti-spam del newsletter.
- “Curaste el catálogo a mano” — anti-genérico, el vendor lo elige.
- “Nos vemos pronto en este sitio.” — cierre editorial con imagen sensorial.
La voz combina tres registros: editorial (Vogue Weddings), operativa (Pro vendor), coloquial (DJ, maquilladora). Cada vertical tiene su variante sin perder coherencia.
Por qué esto es DS y no marketing
Si la voz solo vive en el cerebro del owner, cada landing nueva, cada blog post, cada email transaccional la degrada. Si vive en el sistema (vocabulario prohibido + persona + frases por superficie), la voz sobrevive a la generación automática y al volumen.
bodados publica 4 posts al día sin que el owner los lea. Esto solo es operable porque la voz está codificada como sistema.
12 — Lo que NO se construyó
Para un Design System Architect senior esta sección suele ser la más útil del documento.
| No construido | Por qué | Qué ocupó su lugar |
|---|---|---|
| Suscripción SaaS mensual para pareja | Rompe la tesis “sin cuota mensual, sin contrato”. | Pago único 49 € (después 79 €). |
| App nativa iOS/Android | La invitación se consume desde el browser del invitado. El dashboard es semanal-mensual, no diario. | Web app con PWA implícita. |
| Marketplace público de vendors | Hace de bodados un Bodas.net 2.0. Rompe el modelo de comisión limpia. | Catálogo privado curado por vendor. Sin listado público. |
| GitHub Actions para deploy | Coste no justificado para 5-10 deploys al día. | Deploy manual con Wrangler + Synthetic monitoring CF Worker cron. |
| Storage propio (R2/S3) para imágenes editoriales | Unsplash CDN cubre el 95% de los heros. Reduce mantenimiento. | Unsplash queries + cache CF. R2 reservado para producto post-PMF. |
| Sistema i18n multi-idioma | El mercado es España, vosotros nativo. Inglés diluiría la voz. | Solo es-ES, rechazado conscientemente. |
| Drag-and-drop editor de invitación tipo Canva | Mata la rapidez. El contrato es “elige plantilla + rellena campos”. | Editor de campos con autosave a invitation_configs. 28 templates curados. |
Tabla agent_usage_log en DB | Phase 1 no justifica schema migration. CF Workers Logs es suficiente. | Structured console.log con prefix [agent-telemetry] parseable por Logpush. |
| White-label completo (dominio del cliente) | No aporta a la tesis Phase 1. Implica sales largo. | pro.bodados.com subdominio + dominio personalizado solo para invitación de pareja Pro. |
| Slack notifications para signups | Email a me@joanarbo.com cubre el caso founder-solo. | Resend transactional con retry exponencial. |
| Page builder propio para landings de marketing | Las 8 landings de role son SSG con copy en pro-roles.ts. | Server Components con SSG por slug. |
| Anuncios on-page del vendor en invitación pareja | Contamina la voz editorial. Rompe la confianza. | Branding sutil “Bodados” en footer. |
| Newsletter de pago | Demasiado pronto, audiencia editorial pequeña. | Newsletter gratis en Buttondown, semanal. |
El patrón
Cada “no” preserva una decisión más profunda: bodados es producto editorial wedding-tech con doble audiencia, operado por founder único, con disciplina de DS. No es marketplace. No es SaaS genérico. No es app móvil. No es Bodas.net 2.0.
Aplicación rigurosa del “you are what you say no to”.
13 — Cronología leída desde el repositorio
24 migraciones SQL y commits desde el 18 de febrero de 2026 cuentan la historia del build en ocho fases.
| Fase | Periodo | Qué se construyó |
|---|---|---|
| 1 · Génesis | Febrero 2026 | Next.js 15 App Router + Supabase Auth + invitación editorial (template editorial-main). |
| 2 · Catálogo de templates | Marzo 2026 | 28 plantillas de invitación con motionProfile + layoutProfile. Editor con autosave. |
| 3 · Migración Supabase → Neon | Abril 2026 | Dual-write en 5 tablas. Schema bodados.* en Neon Frankfurt. |
| 4 · Pivote a vendor SaaS | Abril–Mayo 2026 | /pro landing + /pro/[role] con 8 verticales + Stripe Connect Express + mesa de regalos. |
| 5 · Pipeline blog agéntico | Mayo 2026 | Cron 4×/día + critic-loop Reflexion + hard validator. Switch a DeepSeek-V3.1. |
| 6 · Killing Netlify | Mayo 2026 | 5 scheduled functions migradas a CF Workers nativos. Single-vendor. |
| 7 · Auditoría completa | Mayo 2026 | Smoke funnel + Playwright E2E + slug Zod + atomic gift claim + rate-limits + CSP fix + telemetría AI agents. |
| 8 · Brand polish | Mayo 2026 | Logo SVG monograma en 4 surfaces. 28 templates verificados visualmente. |
Lo que esta cronología revela
Cuatro semanas separan la decisión de pivotar a vendor SaaS de tener /pro landing con 8 verticales en producción. Tres semanas separan la idea de blog agéntico de tener critic-loop + validator operando 4×/día. Para un DS lead que opera Phase 1 de producto, este ritmo solo es posible cuando las decisiones arquitectónicas son sólidas desde el día uno: monorepo, tokens compartidos, contenido como código, proveedor único de infra.
14 — Lo que un DS Architect senior se lleva de aquí
Cinco decisiones aplicables a tu propio sistema, ordenadas por dificultad creciente.
1. Catálogo curado en código, no en DB (fácil)
Si tu sistema tiene catálogos críticos (templates, variantes, plantillas, recetas, configuraciones), no los pongas en DB hasta que un editor humano necesite modificarlos sin deploy. Mientras los edita el owner, viven en código como const exportadas. Cambios versionables en git, type safety automática, sin admin UI prematura.
2. Atomic claim antes de side effect externo (fácil-medio)
Cuando tu sistema asigna recursos únicos a usuarios concurrentes (slots, plazas, items, asientos), ejecuta UPDATE WHERE estado_disponible RETURNING antes de tocar el servicio externo (Stripe, calendario, inventario). Sin esto, las race conditions son cuestión de tiempo, no de probabilidad.
3. Dual-write con env-driven mode para migración de DB (medio)
Cuando tu sistema migra infraestructura crítica de DB, mantén dual-write durante el watch period controlado por variable de entorno. Rollback es env var, no migración inversa. Verificación continua compara checksums. Cutover sin downtime.
4. Routing per-subdomain en middleware sin app separada (medio-difícil)
Cuando tu producto tiene dos audiencias con vocabulario muy distinto, no construyas dos apps. Usa middleware que lee host y reescribe rutas sin cambiar URL. Single worker, single bundle, single deploy. Tokens compartidos del DS, componentes especializados por surface.
5. Agentes IA con acción real autenticada sobre estado privado (difícil)
Si tu producto incorpora agentes IA, distingue entre chatbot decorativo y agente con acción real. Los primeros responden preguntas. Los segundos modifican estado del usuario autenticado. Implica auth como token primario (no decoración), vocabulario de acción acotado (tool use estructurado), telemetría que pueda crecer sin schema migration prematura. Es la pieza más difícil porque exige redefinir qué es un componente cuando una de sus fuentes de cambio es un LLM con permisos sobre datos del owner.
Esta última es la pregunta que va a separar al equipo que escala su producto con IA del equipo que añade chatbots cosméticos en los próximos 18 meses.
15 — Anexo técnico
Para quien quiera verificar las afirmaciones del documento contra el código.
Stack
| Capa | Tecnología |
|---|---|
| Frontend | Next.js 15.3 App Router + React 18 + Tailwind 3.4 + framer-motion 12 |
| Hosting | Cloudflare Workers vía OpenNext (bodados-cf app + bodados-cron scheduled) |
| Backend / Functions | 35 Next.js API routes con runtime: "nodejs" en CF Workers (nodejs_compat) |
| DB principal | Neon Postgres Frankfurt (ep-long-salad-al63869v) con schema bodados.* |
| DB editorial / legacy | Supabase Postgres (dual-write durante watch period para tablas auth-coupled) |
| Vector search | pgvector en Neon · 512 dimensiones |
| Embeddings | Voyage AI voyage-3-lite · 512 dim |
| LLM agents | Anthropic Claude Sonnet 4.6 |
| LLM blog generation | DeepSeek-V3.1 vía Together AI |
| Pagos | Stripe + Stripe Connect Express (destination charges) |
| Resend (transactional) + Buttondown (newsletter) | |
| Storage | Unsplash CDN (heros editoriales) + Cloudflare R2 (planificado) |
| Anti-abuse | CF Bot Fight Mode + AI Bots Protection (block) + Crawler Protection + JS Challenge + rate-limit per-IP/user_id |
| Analytics | GA4 + Meta Pixel opcional + Pinterest Tag + /api/track propio → bodados.lead_activity_log |
| Build | OpenNext + Wrangler |
| CI / CD | Deploy manual con Wrangler. Synthetic monitoring CF Worker cron (gratis) |
| Tests | Playwright E2E (11 specs) + smoke-funnel.mjs (10 checks HTTP) + audit-invitation-templates.mjs (28 templates) |
Números del repositorio
- Funciones backend activas: 35
- Migraciones SQL: 24
- Tests Playwright E2E: 11 specs
- Schemas JSON-LD distintos: 27 tipos
- URLs en sitemap: 172 (incluyendo 17 blog posts publicados)
- Cron jobs operativos: 7 schedules en
wrangler.cron.toml - Idiomas con landing nativa: 1 (es-ES)
- Templates de invitación: 28
- AI agents endpoints: 5 (guest, seating, invitation, fashion, legal)
- Vendor roles segmentados: 8 (fotógrafos, planners, floristas, catering, DJs, videógrafos, maquilladoras, venues)
- Páginas dashboard: 37
- Blog posts publicados: 17
- Blog topics en queue: 30
Cron jobs activos
| Cron | Schedule | Función |
|---|---|---|
email-sequence-processor | */15 * * * * | Procesa enrollments con next_send_at vencido vía Resend |
smoke-funnel | */30 * * * * | 10 checks HTTP contra producción |
lead-scoring-daily | 0 3 * * * | Recalcula leads.score con SCORE_WEIGHTS por acción + lifecycle |
blog-generate-scheduled | 0 7,12,17,20 * * * | Pipeline agéntico de generación + critic-loop + validator |
growth-insights-weekly | 0 8 * * 1 | Report semanal a FOUNDER_EMAIL con KPIs |
newsletter-weekly | 0 8 * * 2 | Pulls últimos 4 blog posts + draft a Buttondown |
cleanup-prospects | 0 9 * * * | Borra prospects no contactados >30 días (AEPD §5) |
Los cinco agentes IA
| Agente | Endpoint | Acción real |
|---|---|---|
guest-agent | /api/guest-agent | CRUD sobre bodados.guests |
seating-agent | /api/seating-agent | Asignación con restricciones a bodados.seating_assignments |
invitation-agent | /api/invitation-agent | Modifica bodados.invitation_configs |
fashion-agent | /api/fashion-agent | Sugiere outfits sin escritura |
legal-agent | /api/legal-agent | RAG sobre legal_knowledge con citas trazables |
Pipeline blog agéntico
Cron (07/12/17/20 UTC peak Spain)
→ seleccionar topic de blog_generation_queue (status=pending)
→ DeepSeek-V3.1 vía Together (system prompt persona Alejandra + 9 reglas + vocabulario prohibido)
└ output JSON {slug, title, seoTitle, seoDescription, excerpt, body, evidence[]}
→ critic-loop (rúbrica 5 dimensiones)
└ si score < 7 → revisar una vez (cap 1, paper Reflexion diminishing returns)
→ hard validator (wordcount, H2 count, ratio español, regex anti-LLM)
└ si fail → reject + log + escoger otro topic
→ upsert blog_posts (Supabase canonical + Neon mirror durante dual-write)
→ marcar topic como published
Verificación reproducible
# Inventario público
curl -s https://bodados.com/sitemap.xml | grep -c '<loc>'
# → 172
# Posicionamiento público
curl -s -A "Mozilla/5.0" https://bodados.com | \
grep -oE '<title>[^<]+</title>|name="description" content="[^"]+"' | head -2
# Schemas JSON-LD distintos
grep -rE '"@type":\s*"[A-Z][a-zA-Z]+"' src/ --include="*.tsx" --include="*.ts" | \
grep -oE '"@type":\s*"[A-Z][a-zA-Z]+"' | sort -u | wc -l
# → 27
# Migraciones SQL
find supabase/migrations -name "*.sql" | wc -l
# → 24
# Templates de invitación
grep -cE 'id: "[a-z-]+"' src/components/composed/invitation/templateCatalog.ts
# → 28
# Cron jobs operativos
grep -E '^\s*"' wrangler.cron.toml | grep -c '\*'
# → 7
# AI agents endpoints
find src/app/api -name "route.ts" -path "*-agent*" | wc -l
# → 5
# Tests E2E
ls e2e/*.spec.ts 2>/dev/null && grep -c "^test(" e2e/funnel.spec.ts
# → 11
# Smoke funnel contra producción
node scripts/smoke-funnel.mjs
# → 10/10 OK
URLs públicas verificables:
- Home consumer:
https://bodados.com - Vendor SaaS landing:
https://bodados.com/pro - Vertical catering:
https://bodados.com/pro/catering - Pricing standalone:
https://bodados.com/precios - Blog editorial:
https://bodados.com/blog - Vendor-LTV calculator:
https://bodados.com/calculadora - Demo mesa de regalos:
https://bodados.com/gift/lucia-y-diego
Glosario
| Término | Definición canónica |
|---|---|
| Pareja | Usuario consumer del producto B2C. Siempre en plural (vosotros, no tú). |
| Vendor | Profesional del sector nupcial. Cliente B2B con Stripe Connect Express. |
| Pro | Tier de pago único 49 € (B2C) O subdominio del vendor SaaS B2B. Polisemia intencional. |
| Esencial | Tier gratis para siempre del consumer. |
| Pioneras | Primeras 10 parejas captadas por DM Instagram a precio Pro -10% (44 €). |
| Mesa de regalos | Catálogo curado por el vendor (8-12 items) que la pareja comparte. Comisión 2,9% + 0,50 €. |
| Gift | Item individual de la mesa. Estados: available → reserved → paid. |
| Magic-link | Token UUID en URL del invitado que da acceso a RSVP sin password. |
| Critic-loop | Pipeline de revisión Reflexion-style aplicado al output LLM antes de publicar. |
| Direct-answer block | Bloque de 40-60 palabras tras el primer H2 que responde la pregunta principal. Optimizado para AI Overviews. |
| Evidence | Campo obligatorio en blog posts generados: array {type, value, source?} con datos cuantitativos. |
| Asistente | UI panel embebido en una página del dashboard conectado a un agente IA. |
| After-boda | Módulo del dashboard post-evento. Premium-gated. |
| Dual-write | Patrón de migración: cada escritura va a Supabase (canonical) + Neon (mirror) durante watch period. |
| Atomic claim | UPDATE WHERE status=‘available’ RETURNING como sustituto de SELECT FOR UPDATE. |
Caso bodados.com · Lectura desde dentro del oficio · Versión 1.0 · Julio 2026 Autoría: Joan Arbó · joanarbo.com