Decisiones Arquitectonicas (ADRs)
Registro consolidado de las decisiones arquitectonicas mas relevantes de la plataforma Nvito. Cada ADR documenta el contexto, la decision tomada, las consecuencias y el estado actual.
Sobre este Documento
Este documento sigue el formato Architecture Decision Record (ADR) para registrar las decisiones tecnicas fundamentales de Nvito. Cada registro incluye:
- Contexto: La situacion o problema que motivo la decision.
- Decision: La alternativa elegida y la justificacion.
- Consecuencias: Los efectos positivos y negativos de la decision.
- Estado:
Accepted(vigente),Proposed(en evaluacion) oSuperseded(reemplazada).
Indice de ADRs
| ADR | Titulo | Estado |
|---|---|---|
| ADR-001 | NestJS como framework backend | Accepted |
| ADR-002 | Multi-tenancy con RLS de PostgreSQL | Accepted |
| ADR-003 | PostgreSQL como base de datos principal | Accepted |
| ADR-004 | Clerk para autenticacion | Accepted |
| ADR-005 | Multi-repositorio | Accepted |
| ADR-006 | OpenAI + Claude para generacion IA | Accepted |
| ADR-007 | Email via SMTP/nodemailer | Superseded/Updated |
| ADR-008 | Cloudflare R2 para storage | Accepted |
| ADR-009 | React Native + Expo para Mobile | Accepted |
| ADR-010 | Workflow de aprobacion de invitaciones (state machine) | Accepted |
| ADR-011 | PWA como canal de distribucion alternativo | Accepted |
ADR-001: NestJS como Framework Backend
Estado: Accepted
Contexto
Nvito necesitaba un framework backend en Node.js/TypeScript capaz de soportar una API REST con mas de 30 modulos de dominio. Las alternativas evaluadas fueron Express.js (minimalista), Fastify (enfocado en rendimiento) y NestJS (arquitectura modular con DI, guards e interceptors). El equipo requeria estructura para organizar el codigo a escala, con soporte nativo para inyeccion de dependencias, decoradores y middleware.
Decision
Se eligio NestJS 11.x como framework principal del backend (nvito-api) por:
- Arquitectura modular: Cada dominio (Events, Guests, Invitations, AI, etc.) se encapsula en un modulo independiente con controller, service y DTOs.
- Inyeccion de dependencias nativa: Desacopla servicios y facilita testing con mocks.
- Guards e interceptors globales: La cadena de seguridad (ClerkAuthGuard, TenantMiddleware, RoleGuard, PermissionsGuard, AuditInterceptor) se implementa de forma declarativa.
- Decoradores personalizados:
@Public(),@Roles(),@RequirePermission()simplifican la autorizacion. - Ecosistema maduro: Integracion oficial con Prisma, BullMQ, Swagger y Throttler.
Consecuencias
Positivas: Estructura predecible, separacion en capas reforzada (Controller -> Service -> Prisma), Swagger auto-generado, seguridad en capas sin acoplar auth a logica de negocio.
Negativas: Curva de aprendizaje mas pronunciada que Express, mayor overhead de abstracciones, boilerplate extenso por modulo.
ADR-002: Multi-tenancy con RLS de PostgreSQL
Estado: Accepted
Contexto
Nvito es SaaS multi-tenant donde cada organizacion es un tenant aislado. Se evaluaron tres estrategias: database-per-tenant (una BD por organizacion), schema-per-tenant (un schema PostgreSQL por organizacion) y Row Level Security (una BD, un schema, politicas de seguridad a nivel de fila).
Decision
Se eligio RLS con columna organizationId en tablas principales y variable de sesion app.current_tenant_id configurada por el TenantMiddleware via SET LOCAL por transaccion.
- Simplicidad operativa: Un solo backup, una sola conexion Prisma, una sola migracion.
- Aislamiento a nivel de motor: PostgreSQL filtra automaticamente por tenant en cada query, eliminando bugs de acceso cross-tenant.
- Costo eficiente: Una sola instancia es suficiente para el volumen actual del MVP.
- Transaccional:
SET LOCALlimita el contexto de tenant a la transaccion actual.
Consecuencias
Positivas: Aislamiento transparente y automatico, migraciones unificadas, Super Admins operan sin restriccion RLS, prevencion de SQL injection via tagged templates.
Negativas: Dificil mover un tenant a BD dedicada si crece masivamente, queries cross-tenant requieren deshabilitar RLS, estrategia no portable a otros motores, tests requieren configurar variable de sesion.
ADR-003: PostgreSQL como Base de Datos Principal
Estado: Accepted
Contexto
Nvito maneja datos altamente relacionales (organizaciones -> eventos -> invitaciones -> secciones -> templates; invitados -> grupos -> RSVPs). Se evaluaron MySQL, MongoDB y PostgreSQL.
Decision
Se eligio PostgreSQL 15 con Prisma 5.22 como ORM por:
- Row Level Security: Requisito critico para ADR-002. Ni MySQL ni MongoDB ofrecen equivalente nativo.
- Tipos avanzados:
UUIDnativo,JSONBpara schemas de invitaciones,ENUMpara estados,TIMESTAMPTZpara zonas horarias. - JSONB para InvitationSchemaV2: Flexibilidad documental con garantias relacionales.
- Integridad referencial: Foreign keys, constraints compuestos, indices parciales.
- Ecosistema Prisma: Soporte de primera clase con migraciones y generacion de tipos TypeScript.
Consecuencias
Positivas: RLS para multi-tenancy seguro, JSONB combina flexibilidad con garantias relacionales, Prisma genera tipos completos, funciones y triggers para automatizacion.
Negativas: Mayor consumo de recursos que MySQL, tuning requiere conocimiento especializado, Prisma tiene limitaciones en queries complejas (requiere $queryRaw), JSONB dificulta migraciones si el formato cambia radicalmente.
ADR-004: Clerk para Autenticacion
Estado: Accepted
Contexto
Se necesitaba autenticacion con registro, login, JWT, webhooks y UI embebida. Alternativas: Auth0 (pricing por MAU), NextAuth.js (open-source para Next.js), JWT custom (desde cero) y Clerk (componentes UI pre-construidos + SDK React/Next.js).
Decision
Se eligio Clerk con @clerk/nextjs (v6.x) en frontend y @clerk/clerk-sdk-node en backend por:
- Componentes UI pre-construidos:
<SignIn>,<SignUp>,<UserButton>eliminan desarrollo de pantallas de auth. - SDK nativo para Next.js: Middleware, hooks y
ClerkProviderse integran con App Router. - JWT estandar: El backend verifica tokens con claves publicas de Clerk.
- Webhooks de sincronizacion:
POST /webhooks/clerkmantiene tablauserslocal sincronizada. - Velocidad de implementacion: Autenticacion funcional en horas.
Consecuencias
Positivas: Auth completa con minimo esfuerzo, UI profesional, social login y MFA disponibles, ClerkAuthGuard resuelve contexto del usuario en cada request.
Negativas: Vendor lock-in significativo (migrar requiere reescribir guards, webhooks y UI), costo escalable por MAU, latencia por dependencia externa, modelo de datos dual (Clerk + tabla local) con riesgo de inconsistencias.
ADR-005: Multi-repositorio
Estado: Accepted
Contexto
Nvito tiene cuatro aplicaciones con tecnologias y ciclos de deploy distintos. Alternativas: monorepo con Turborepo (workspaces + cache compartido), monorepo con Nx (graph de dependencias) y multi-repo (repositorios independientes).
Decision
Se eligio multi-repositorio con repos independientes en GitLab (nvito-api, nvito-admin, nvito-invitations, nvito-client) por:
- Despliegue independiente: Cada app tiene su pipeline CI/CD. Un cambio en admin no requiere redeploy de api.
- Aislamiento de dependencias: Sin conflictos de versiones entre NestJS y Next.js.
- Simplicidad operativa: Equipo pequeno en fase MVP, no justifica overhead de Turborepo/Nx.
- Versionado independiente: Cada app avanza a su propio ritmo.
Consecuencias
Positivas: CI/CD simple, sin configuracion de workspaces, cada repo con su branching strategy, deploys a Coolify independientes.
Negativas: Duplicacion de tipos entre repos (DTOs, enums), sin build cache compartido, sincronizacion manual ante breaking changes, utilidades comunes duplicadas, posible deuda tecnica al crecer el equipo.
ADR-006: OpenAI + Claude para Generacion IA
Estado: Accepted
Contexto
La generacion de invitaciones con IA desde lenguaje natural es funcionalidad core. Alternativas: solo OpenAI, solo Anthropic, Google Gemini y estrategia multi-proveedor.
Decision
Se implemento estrategia multi-proveedor con OpenAI (GPT-4o) como principal y Anthropic (Claude Sonnet 4.6) como alternativa, con interfaz comun AIProvider, seleccion por usuario y circuit breakers (opossum) independientes (OpenAI: timeout 30s; Claude: timeout 120s; ambos con 50% error threshold y 60s reset).
- Resiliencia: Fallback automatico si un proveedor falla.
- Calidad diversificada: GPT-4o para respuestas rapidas y Vision; Claude para textos emotivos largos.
- Flexibilidad: Parametro
provider(openai,claude,auto) en el endpoint de generacion. - Extensibilidad: La interfaz
AIProviderfacilita agregar proveedores futuros.
Consecuencias
Positivas: Alta disponibilidad, usuarios eligen proveedor, tracking de tokens y latencia por generacion, facil agregar nuevos proveedores.
Negativas: Complejidad de mantener dos SDKs (openai + @anthropic-ai/sdk), diferencias en output entre proveedores, testing debe cubrir ambos proveedores y estados del circuit breaker.
ADR-007: Email via SMTP/nodemailer
Estado: Superseded/Updated
Contexto
Nvito requiere email transaccional para RSVP, invitaciones, recordatorios y campanas masivas. La decision original fue usar Resend por su API simple. Durante desarrollo se identificaron limitaciones para cambiar entre proveedores SMTP por entorno, y se migro a nodemailer.
Decision
Se migro a nodemailer 8.x con configuracion SMTP parametrizada por variables de entorno por:
- Flexibilidad: Agnostico al proveedor; apunta a MailDev (local), Mailtrap (testing) o SMTP de produccion sin cambiar codigo.
- Autenticacion condicional: Soporta SMTP sin auth (MailDev, puerto 1025) y con auth (Mailtrap/produccion, puerto 587).
- Procesamiento asincrono: Emails encolados via BullMQ (
EmailProcessor) con reintentos y backoff exponencial. - Templates Handlebars: Personalizacion completa de emails.
| Entorno | Proveedor | Puerto | Auth |
|---|---|---|---|
| LOCAL | MailDev | 1025 | No |
| DEV / TEST | Mailtrap | 587 | Si |
| PRODUCTION | Por definir | 587 | Si |
Consecuencias
Positivas: Sin vendor lock-in, entorno local sin API keys, Mailtrap previene envios accidentales a usuarios reales, reintentos automaticos.
Negativas: Sin tracking nativo de opens/clicks/bounces, requiere configurar SPF/DKIM/DMARC para produccion, sin dashboard de monitoreo, deliverability es responsabilidad del equipo.
ADR-008: Cloudflare R2 para Storage
Estado: Accepted
Contexto
Se necesita almacenamiento de objetos para imagenes, audio, HTML compilado y archivos privados, organizados en 4 buckets. Alternativas: AWS S3, Google Cloud Storage y Cloudflare R2.
Decision
Se eligio Cloudflare R2 para produccion y MinIO para desarrollo local, ambos accedidos via AWS SDK v3 (@aws-sdk/client-s3) por:
- Sin costos de egress: R2 no cobra transferencia de salida, ahorro significativo al servir multimedia a miles de invitados.
- Compatibilidad S3: Mismo
S3Clientpara R2 y MinIO; cambiar proveedor es cuestion de endpoint y credenciales. - CDN integrado: Buckets publicos servidos desde edge de Cloudflare sin CDN adicional.
- Upload directo: Presigned URLs eliminan la necesidad de que archivos pasen por el backend.
- MinIO local: Contenedor Docker con API S3 para desarrollo offline.
Consecuencias
Positivas: Costos menores que S3, desarrollo offline con MinIO, un solo StorageService para ambos proveedores, CDN integrado, validacion de archivos por magic bytes independiente del proveedor.
Negativas: Ecosistema menor que S3, funcionalidades limitadas (sin Object Lock, S3 Select), dependencia concentrada en Cloudflare, diferencias sutiles con S3 en headers y CORS.
ADR-009: React Native + Expo para Mobile
Estado: Accepted
Contexto
El 85%+ del trafico de invitaciones es movil. La web responsive cubrio las necesidades del MVP inicial, pero se identificaron funcionalidades que requerian capacidades nativas: notificaciones push, acceso a camara para QR check-in, experiencia offline para el dia del evento, grabacion de audio (guestbook) y pagos nativos. Alternativas evaluadas: Flutter (Dart, rendimiento nativo), PWA-only (sin app nativa) y React Native (React + TypeScript cross-platform).
Decision
Se implemento nvito-client con React Native 0.81.5 + Expo 54 como app movil nativa (iOS/Android) por:
- Reutilizacion de conocimiento: El equipo domina React y TypeScript; la curva de aprendizaje fue minima.
- Reutilizacion de patrones: TanStack React Query 5, Zod 4, query key factories y el patron de API client se reutilizaron directamente.
- Expo: Simplifica acceso a APIs nativas (camara, audio, notificaciones, secure storage) sin configuracion nativa manual.
- NativeWind 4: Tailwind CSS compilado a StyleSheet nativo, consistencia visual con nvito-admin.
Stack implementado:
| Tecnologia | Version | Proposito |
|---|---|---|
| React Native | 0.81.5 | Framework de UI nativa |
| Expo | 54.x | Plataforma de desarrollo y distribucion |
| Expo Router | 6.x | Navegacion file-based con layouts anidados |
| NativeWind | 4.x | Tailwind CSS para React Native |
| TanStack React Query | 5.x | Cache, fetching, persistencia offline |
| expo-camera | 17.x | Escaneo QR para check-in |
| expo-av | 16.x | Grabacion y reproduccion de audio |
| expo-notifications | 0.32.x | Push notifications via Expo Push API |
| expo-secure-store | 15.x | Almacenamiento encriptado de tokens |
| Stripe React Native | 0.50.x | Pagos nativos (cash fund) |
| Zod | 4.x | Validacion de schemas |
Estructura de tabs por rol:
| Rol | Tabs visibles | Funcionalidad principal |
|---|---|---|
| HOST (5 tabs) | Dashboard, Invitados, Scanner, Galeria, Mas | Monitoreo en vivo, check-in QR (offline), aprobar audios, gestionar galeria |
| GUEST (4 tabs) | Inicio, Programa, Galeria, Interactuar | Ver QR pass, itinerario, subir fotos, dejar mensajes de voz, contribuir a cash fund |
Autenticacion independiente de Clerk:
La app movil no usa Clerk; implementa JWT propio firmado con MOBILE_JWT_SECRET:
- HOST: login con codigo de evento + PIN -> JWT de 1h + refresh token de 30 dias
- GUEST: login con access token de invitado -> mismo esquema de tokens
- Tokens almacenados en
expo-secure-store(encriptacion nativa del dispositivo) - Refresh proactivo cuando el token esta a < 2 minutos de expirar
- Restauracion automatica de sesion al abrir la app
Soporte offline:
- Cola de operaciones en AsyncStorage (
offlineQueue) para check-ins offline - Validacion local de QR contra cache de passes descargados
- Sincronizacion automatica al recuperar conexion (
useOfflineSync) - React Query persistido en AsyncStorage (7 dias de cache, stale time de 5 min)
- Deteccion de conectividad via
@react-native-community/netinfo
Deep linking:
- Scheme:
nvito:// - Universal links:
https://nvito.app/join?accessToken=XXX - Intent filters Android con autoVerify para
nvito.app/join
Consecuencias
Positivas (confirmadas): Tiempo de desarrollo significativamente menor que Flutter o nativo puro, 0 necesidad de aprender Dart, patrones compartidos con nvito-admin (React Query, Zod, query keys), capacidades nativas completas (push, camara, audio, offline, pagos), layouts responsive para tablet y telefono.
Negativas (confirmadas): Cuarto repositorio incrementa complejidad del ecosistema (ver ADR-005), tipos no compartidos directamente entre repos (duplicacion manual), Expo limita acceso a algunas APIs nativas avanzadas, hot reload ocasionalmente requiere restart completo.
ADR-010: Workflow de Aprobacion de Invitaciones (State Machine)
Estado: Accepted
Contexto
Las invitaciones pasan por multiples etapas antes de ser publicas. Se necesitaba un mecanismo que definiera estados, controlara transiciones y sincronizara con el ciclo de vida de eventos. Alternativas: flags booleanos (isPublished, isDraft), estado simple con validacion ad-hoc y state machine formal.
Decision
Se implemento un patron de maquina de estados via ApprovalWorkflowService con 4 estados y 6 transiciones:
| De | A | Accion | Validacion principal |
|---|---|---|---|
IN_CONSTRUCTION | UNPUBLISHED | Submit | Schema no vacio, secciones requeridas |
UNPUBLISHED | PUBLISHED | Approve | Genera HTML, sube a CDN, activa evento |
UNPUBLISHED | IN_CONSTRUCTION | Reject | Requiere razon de rechazo |
PUBLISHED | UNPUBLISHED | Unpublish | Webhook de despublicacion |
PUBLISHED | CLOSED | Close | Registra closedBy y closeReason |
CLOSED | PUBLISHED | Reopen | Evento ACTIVE, fecha no pasada |
Razones:
- Prevencion de estados invalidos: No se puede publicar sin pasar por revision.
- Validaciones por transicion: Cada transicion tiene reglas propias.
- Sincronizacion con eventos: Publicar invitacion activa evento (
DRAFT->ACTIVE); cancelar evento cierra invitaciones. - Auditabilidad: Cada transicion queda en
audit_log. - Cierre automatico: Cron diario (medianoche,
America/Mexico_City) cierra invitaciones de eventos pasados.
Consecuencias
Positivas: Estado siempre consistente, transiciones invalidas rechazadas a nivel de servicio, sincronizacion bidireccional invitaciones-eventos, historial de transiciones auditable, cierre automatico previene invitaciones obsoletas.
Negativas: Complejidad adicional que debe sincronizarse con endpoints y UI, agregar estados/transiciones no es trivial, UI del frontend debe reflejar correctamente acciones disponibles, testing exhaustivo de todas las combinaciones.
ADR-011: PWA como Canal de Distribucion Alternativo
Estado: Accepted
Contexto
La app movil nativa (nvito-client, React Native + Expo) ofrece la experiencia completa el dia del evento: check-in QR, galeria, audio guestbook, push notifications. Sin embargo, la distribucion es una brecha critica:
- Friccion de instalacion: Anfitriones e invitados no siempre quieren o pueden instalar la app desde App Store/Play Store
- Ciclo de aprobacion de stores: Las actualizaciones criticas pueden tardar dias en ser aprobadas
- Deep links rotos: Sin la app instalada, los links compartidos no funcionan
- Cobertura limitada: Usuarios casuales (invitados de una boda) no instalaran una app para un uso unico
Decision
Se creo nvito-pwa como Progressive Web App complementaria a la app nativa. Decisiones clave:
- Next.js 16 con App Router: Consistente con nvito-admin e nvito-invitations
- Patron BFF (Backend For Frontend): JWT tokens nunca llegan al JavaScript del browser. Se encriptan con AES-256-GCM en cookies HttpOnly y se desencriptan solo en el servidor (route handlers de Next.js)
- Proxy catch-all: Todas las llamadas a nvito-api pasan por
/api/proxy/*que inyectaAuthorization: Bearerdesde la cookie encriptada - Coexistencia con app nativa: Ambas apps consumen los mismos 33 endpoints moviles (
/v1/mobile/*) sin modificaciones al backend - Dominio separado:
app.nvito.mxpara la PWA, independiente del admin y las invitaciones
Stack
| Tecnologia | Version | Proposito |
|---|---|---|
| Next.js | 16.x | Framework + BFF |
| TanStack Query | 5.x | Cache y fetching |
| Zod | 4.x | Validacion |
| Tailwind CSS | v4 | Estilos |
| Vitest | 4.x | Testing (18 suites, 204 tests) |
Consecuencias
Positivas:
- Distribucion instantanea via URL, sin instalacion
- Actualizaciones inmediatas sin aprobacion de stores
- Deep links siempre funcionan (fallback PWA)
- Seguridad superior al no exponer tokens al browser
Negativas:
- Mantener dos clientes (nativo + PWA) con funcionalidad similar
- Algunas limitaciones web vs nativo (push en iOS solo con PWA instalada, haptics limitados)
- Complejidad adicional del patron BFF vs llamadas directas al API
Relacion con ADR-009
React Native sigue vigente como primera opcion para la experiencia nativa completa. La PWA es un canal alternativo que coexiste sin reemplazar la app nativa.
Referencias
| Recurso | Ubicacion |
|---|---|
| Arquitectura del Sistema | Sistema |
| Diagramas de Arquitectura | Diagramas |
| ADR Format (Michael Nygard) | https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions |