Docs

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) 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 autenticacionAccepted
ADR-005Multi-repositorioAccepted
ADR-006OpenAI + Claude para generacion 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 distribucion 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 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:

  1. Arquitectura modular: Cada dominio (Events, Guests, Invitations, AI, etc.) se encapsula en un modulo independiente con controller, service y DTOs.
  2. Inyeccion de dependencias nativa: Desacopla servicios y facilita 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 autorizacion.
  5. 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.

  1. Simplicidad operativa: Un solo backup, una sola conexion Prisma, una sola migracion.
  2. Aislamiento a nivel de motor: PostgreSQL filtra automaticamente 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 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:

  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, indices parciales.
  5. 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:

  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 publicas de Clerk.
  4. Webhooks de sincronizacion: POST /webhooks/clerk mantiene tabla users local sincronizada.
  5. 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:

  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. 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).

  1. Resiliencia: Fallback automatico si un proveedor falla.
  2. Calidad diversificada: GPT-4o para respuestas rapidas y Vision; Claude para textos emotivos largos.
  3. Flexibilidad: Parametro provider (openai, claude, auto) en el endpoint de generacion.
  4. Extensibilidad: La interfaz AIProvider facilita 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:

  1. Flexibilidad: Agnostico al proveedor; apunta a MailDev (local), Mailtrap (testing) o SMTP de produccion sin cambiar codigo.
  2. Autenticacion condicional: Soporta SMTP sin auth (MailDev, puerto 1025) y con auth (Mailtrap/produccion, puerto 587).
  3. Procesamiento asincrono: Emails encolados via BullMQ (EmailProcessor) con reintentos y backoff exponencial.
  4. Templates Handlebars: Personalizacion 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 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:

  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 publicos 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, 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:

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

Stack implementado:

TecnologiaVersionProposito
React Native0.81.5Framework de UI nativa
Expo54.xPlataforma de desarrollo y distribucion
Expo Router6.xNavegacion 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 reproduccion 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.xValidacion de schemas

Estructura de tabs por rol:

RolTabs visiblesFuncionalidad principal
HOST (5 tabs)Dashboard, Invitados, Scanner, Galeria, MasMonitoreo en vivo, check-in QR (offline), aprobar audios, gestionar galeria
GUEST (4 tabs)Inicio, Programa, Galeria, InteractuarVer 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:

DeAAccionValidacion principal
IN_CONSTRUCTIONUNPUBLISHEDSubmitSchema no vacio, secciones requeridas
UNPUBLISHEDPUBLISHEDApproveGenera HTML, sube a CDN, activa evento
UNPUBLISHEDIN_CONSTRUCTIONRejectRequiere razon de rechazo
PUBLISHEDUNPUBLISHEDUnpublishWebhook de despublicacion
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 transicion: Cada transicion tiene reglas propias.
  3. Sincronizacion con eventos: Publicar invitacion activa evento (DRAFT -> ACTIVE); cancelar evento cierra invitaciones.
  4. Auditabilidad: Cada transicion queda en audit_log.
  5. 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:

  1. Friccion de instalacion: 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 dias 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

TecnologiaVersionProposito
Next.js16.xFramework + BFF
TanStack Query5.xCache y fetching
Zod4.xValidacion
Tailwind CSSv4Estilos
Vitest4.xTesting (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

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