Docs

Decisiones Arquitectonicas (ADR)

Registro consolidado de las decisiones arquitectonicas más relevantes de la plataforma Nvito, siguiendo el formato Architecture Decision Record.

PublicadoMarzo 2026Equipo de desarrollo, arquitectos, stakeholders

Sobre este Documento

Este documento sigue el formato Architecture Decision Record (ADR) para registrar las decisiones técnicas fundamentales de Nvito. Cada registro incluye:

  • Contexto: La situacion o problema que motivo la decision.
  • Decision: La alternativa elegida y la justificación.
  • Consecuencias: Los efectos positivos y negativos de la decision.
  • Estado: Accepted (vigente), Proposed (en evaluacion) o Superseded (reemplazada).

Indice de ADRs

ADRTituloEstado
ADR-001NestJS como framework backendAccepted
ADR-002Multi-tenancy con RLS de PostgreSQLAccepted
ADR-003PostgreSQL como base de datos principalAccepted
ADR-004Clerk para autenticaciónAccepted
ADR-005Multi-repositorioAccepted
ADR-006OpenAI + Claude para generación IAAccepted
ADR-007Email via SMTP/nodemailerSuperseded/Updated
ADR-008Cloudflare R2 para storageAccepted
ADR-009React Native + Expo para MobileAccepted
ADR-010Workflow de aprobacion de invitaciones (state machine)Accepted
ADR-011PWA como canal de distribución alternativoAccepted

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 más de 30 módulos de dominio. Las alternativas evaluadas fueron Express.js (mínimalista), Fastify (enfocado en rendimiento) y NestJS (arquitectura modular con DI, guards e interceptors). El equipo requeria estructura para organizar el código a escala, con soporte nativo para inyección de dependencias, decoradores y middleware.

Decision

Se eligio NestJS 11.x como framework principal del backend (nvito-api) por:

  1. Arquitectura modular: Cada dominio (Events, Guests, Invitations, AI, etc.) se encapsula en un módulo independiente con controller, service y DTOs.
  2. Inyección de dependencias nativa: Desacopla servicios y fácilita testing con mocks.
  3. Guards e interceptors globales: La cadena de seguridad (ClerkAuthGuard, TenantMiddleware, RoleGuard, PermissionsGuard, AuditInterceptor) se implementa de forma declarativa.
  4. Decoradores personalizados: @Public(), @Roles(), @RequirePermission() simplifican la autorización.
  5. Ecosistema maduro: Integración 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 lógica de negocio.

Negativas: Curva de aprendizaje más 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 organización es un tenant aislado. Se evaluaron tres estrategias: database-per-tenant (una BD por organización), schema-per-tenant (un schema PostgreSQL por organización) 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 transacción.

  1. Simplicidad operativa: Un solo backup, una sola conexión Prisma, una sola migración.
  2. Aislamiento a nivel de motor: PostgreSQL filtra automáticamente por tenant en cada query, eliminando bugs de acceso cross-tenant.
  3. Costo eficiente: Una sola instancia es suficiente para el volumen actual del MVP.
  4. Transaccional: SET LOCAL limita el contexto de tenant a la transacción actual.

Consecuencias

Positivas: Aislamiento transparente y automático, 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:

  1. Row Level Security: Requisito critico para ADR-002. Ni MySQL ni MongoDB ofrecen equivalente nativo.
  2. Tipos avanzados: UUID nativo, JSONB para schemas de invitaciones, ENUM para estados, TIMESTAMPTZ para zonas horarias.
  3. JSONB para InvitationSchemaV2: Flexibilidad documental con garantias relacionales.
  4. Integridad referencial: Foreign keys, constraints compuestos, índices parciales.
  5. Ecosistema Prisma: Soporte de primera clase con migraciones y generación 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 Autenticación

Estado: Accepted

Contexto

Se necesitaba autenticación 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:

  1. Componentes UI pre-construidos: <SignIn>, <SignUp>, <UserButton> eliminan desarrollo de pantallas de auth.
  2. SDK nativo para Next.js: Middleware, hooks y ClerkProvider se integran con App Router.
  3. JWT estandar: El backend verifica tokens con claves públicas de Clerk.
  4. Webhooks de sincronización: POST /webhooks/clerk mantiene tabla users local sincronizada.
  5. Velocidad de implementación: Autenticación funcional en horas.

Consecuencias

Positivas: Auth completa con mínimo 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:

  1. Despliegue independiente: Cada app tiene su pipeline CI/CD. Un cambio en admin no requiere redeploy de api.
  2. Aislamiento de dependencias: Sin conflictos de versiones entre NestJS y Next.js.
  3. Simplicidad operativa: Equipo pequeno en fase MVP, no justifica overhead de Turborepo/Nx.
  4. Versiónado independiente: Cada app avanza a su propio ritmo.

Consecuencias

Positivas: CI/CD simple, sin configuración de workspaces, cada repo con su branching strategy, deploys a Coolify independientes.

Negativas: Duplicacion de tipos entre repos (DTOs, enums), sin build cache compartido, sincronización manual ante breaking changes, utilidades comunes duplicadas, posible deuda técnica al crecer el equipo.


ADR-006: OpenAI + Claude para Generación IA

Estado: Accepted

Contexto

La generación 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 común AIProvider, seleccion por usuario y circuit breakers (opossum) independientes (OpenAI: timeout 30s; Claude: timeout 120s; ambos con 50% error threshold y 60s reset).

  1. Resiliencia: Fallback automático si un proveedor falla.
  2. Calidad diversificada: GPT-4o para respuestas rápidas y Vision; Claude para textos emotivos largos.
  3. Flexibilidad: Parametro provider (openai, claude, auto) en el endpoint de generación.
  4. Extensibilidad: La interfaz AIProvider fácilita agregar proveedores futuros.

Consecuencias

Positivas: Alta disponibilidad, usuarios eligen proveedor, tracking de tokens y latencia por generación, fácil 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 configuración SMTP parametrizada por variables de entorno por:

  1. Flexibilidad: Agnostico al proveedor; apunta a MailDev (local), Mailtrap (testing) o SMTP de producción sin cambiar código.
  2. Autenticación condicional: Soporta SMTP sin auth (MailDev, puerto 1025) y con auth (Mailtrap/producción, puerto 587).
  3. Procesamiento asincrono: Emails encolados via BullMQ (EmailProcessor) con reintentos y backoff exponencial.
  4. Templates Handlebars: Personalización completa de emails.
EntornoProveedorPuertoAuth
LOCALMailDev1025No
DEV / TESTMailtrap587Si
PRODUCTIONPor definir587Si

Consecuencias

Positivas: Sin vendor lock-in, entorno local sin API keys, Mailtrap previene envios accidentales a usuarios reales, reintentos automáticos.

Negativas: Sin tracking nativo de opens/clicks/bounces, requiere configurar SPF/DKIM/DMARC para producción, 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 producción y MinIO para desarrollo local, ambos accedidos via AWS SDK v3 (@aws-sdk/client-s3) por:

  1. Sin costos de egress: R2 no cobra transferencia de salida, ahorro significativo al servir multimedia a miles de invitados.
  2. Compatibilidad S3: Mismo S3Client para R2 y MinIO; cambiar proveedor es cuestion de endpoint y credenciales.
  3. CDN integrado: Buckets públicos servidos desde edge de Cloudflare sin CDN adicional.
  4. Upload directo: Presigned URLs eliminan la necesidad de que archivos pasen por el backend.
  5. 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, validación 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 sútiles 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 día 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:

  1. Reutilizacion de conocimiento: El equipo domina React y TypeScript; la curva de aprendizaje fue mínima.
  2. Reutilizacion de patrones: TanStack React Query 5, Zod 4, query key factories y el patrón de API client se reutilizaron directamente.
  3. Expo: Simplifica acceso a APIs nativas (camara, audio, notificaciones, secure storage) sin configuración nativa manual.
  4. NativeWind 4: Tailwind CSS compilado a StyleSheet nativo, consistencia visual con nvito-admin.

Stack implementado:

TecnologiaVersiónProposito
React Native0.81.5Framework de UI nativa
Expo54.xPlataforma de desarrollo y distribución
Expo Router6.xNavegación file-based con layouts anidados
NativeWind4.xTailwind CSS para React Native
TanStack React Query5.xCache, fetching, persistencia offline
expo-camera17.xEscaneo QR para check-in
expo-av16.xGrabacion y reproducción de audio
expo-notifications0.32.xPush notifications via Expo Push API
expo-secure-store15.xAlmacenamiento encriptado de tokens
Stripe React Native0.50.xPagos nativos (cash fund)
Zod4.xValidación de schemas

Estructura de tabs por rol:

RolTabs visiblesFuncionalidad principal
HOST (5 tabs)Dashboard, Invitados, Scanner, Galería, MasMonitoreo en vivo, check-in QR (offline), aprobar audios, gestionar galería
GUEST (4 tabs)Inicio, Programa, Galería, InteractuarVer QR pass, itinerario, subir fotos, dejar mensajes de voz, contribuir a cash fund

Autenticación independiente de Clerk:

La app movil no usa Clerk; implementa JWT propio firmado con MOBILE_JWT_SECRET:

  • HOST: login con código 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 está a < 2 minutos de expirar
  • Restauración automática de sesion al abrir la app

Soporte offline:

  • Cola de operaciones en AsyncStorage (offlineQueue) para check-ins offline
  • Validación local de QR contra cache de passes descargados
  • Sincronización automática al recuperar conexión (useOfflineSync)
  • React Query persistido en AsyncStorage (7 días de cache, stale time de 5 min)
  • Detección 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 teléfono.

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 múltiples etapas antes de ser públicas. 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 validación ad-hoc y state machine formal.

Decision

Se implemento un patrón de maquina de estados via ApprovalWorkflowService con 4 estados y 6 transiciones:

DeAAcciónValidación principal
IN_CONSTRUCTIONUNPUBLISHEDSubmitSchema no vacio, secciones requeridas
UNPUBLISHEDPUBLISHEDApproveGenera HTML, sube a CDN, activa evento
UNPUBLISHEDIN_CONSTRUCTIONRejectRequiere razon de rechazo
PUBLISHEDUNPUBLISHEDUnpublishWebhook de despublicación
PUBLISHEDCLOSEDCloseRegistra closedBy y closeReason
CLOSEDPUBLISHEDReopenEvento ACTIVE, fecha no pasada

Razones:

  1. Prevencion de estados invalidos: No se puede publicar sin pasar por revision.
  2. Validaciones por transición: Cada transición tiene reglas propias.
  3. Sincronización con eventos: Publicar invitación activa evento (DRAFT -> ACTIVE); cancelar evento cierra invitaciones.
  4. Auditabilidad: Cada transición queda en audit_log.
  5. Cierre automático: Cron diario (medianoche, America/Mexico_City) cierra invitaciones de eventos pasados.

Consecuencias

Positivas: Estado siempre consistente, transiciones invalidas rechazadas a nivel de servicio, sincronización bidireccional invitaciones-eventos, historial de transiciones auditable, cierre automático 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 día del evento: check-in QR, galería, audio guestbook, push notifications. Sin embargo, la distribución es una brecha critica:

  1. Friccion de instalación: Anfitriones e invitados no siempre quieren o pueden instalar la app desde App Store/Play Store
  2. Ciclo de aprobacion de stores: Las actualizaciones criticas pueden tardar días en ser aprobadas
  3. Deep links rotos: Sin la app instalada, los links compartidos no funcionan
  4. 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:

  1. Next.js 16 con App Router: Consistente con nvito-admin e nvito-invitations
  2. 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)
  3. Proxy catch-all: Todas las llamadas a nvito-api pasan por /api/proxy/* que inyecta Authorization: Bearer desde la cookie encriptada
  4. Coexistencia con app nativa: Ambas apps consumen los mismos 33 endpoints moviles (/v1/mobile/*) sin modificaciones al backend
  5. Dominio separado: app.nvito.mx para la PWA, independiente del admin y las invitaciones

Stack

TecnologiaVersiónProposito
Next.js16.xFramework + BFF
TanStack Query5.xCache y fetching
Zod4.xValidación
Tailwind CSSv4Estilos
Vitest4.xTesting (18 suites, 204 tests)

Consecuencias

Positivas:

  • Distribucion instantanea via URL, sin instalación
  • 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 patrón BFF vs llamadas directas al API

Relación con ADR-009

React Native sigue vigente como primera opción para la experiencia nativa completa. La PWA es un canal alternativo que coexiste sin reemplazar la app nativa.


Referencias

RecursoUbicación
Arquitectura del SistemaSistema
Diagramas de ArquitecturaDiagramas
ADR Format (Michael Nygard)https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions
Esta pagina fue util?