Docs

nvito-pwa — Progressive Web App

PWA que replica la funcionalidad de la app movil nativa (nvito-client), accesible via URL sin instalacion desde tiendas de aplicaciones.

CampoValor
Version1.0
StackNext.js 16.x, React 19, TypeScript 5, Tailwind v4
Puerto local3002
Dominioapp.nvito.mx
EstadoEn desarrollo

1. Proposito

La PWA resuelve una brecha critica en la distribucion: los invitados y anfitriones no siempre quieren o pueden instalar la app nativa. La PWA ofrece la misma funcionalidad accesible desde cualquier navegador moderno via URL.

Coexistencia con la app nativa: La PWA no reemplaza a nvito-client. Ambas apps comparten los mismos 33 endpoints moviles (/v1/mobile/*) del backend.

2. Arquitectura BFF (Backend For Frontend)

La PWA implementa un patron BFF donde los JWT tokens nunca llegan al JavaScript del browser:

Browser → /api/proxy/* (BFF) → Authorization: Bearer <jwt> → nvito-api

Flujo de autenticacion

  1. Usuario ingresa credenciales en el formulario de login
  2. Browser envía POST a /api/auth/login (BFF route handler)
  3. BFF valida con Zod, aplica rate limiting, forward a nvito-api /v1/mobile/auth/login
  4. BFF recibe JWT tokens de la respuesta
  5. BFF encripta tokens con AES-256-GCM y los almacena en cookies HttpOnly
  6. BFF retorna al browser solo metadata de sesion (role, eventId) — SIN tokens

Flujo proxy (cada request)

  1. Browser → GET /api/proxy/events/:id (con cookie HttpOnly automatica)
  2. BFF valida CSRF (si es mutacion), desencripta JWT de la cookie
  3. Si JWT expira en < 2 minutos → refresh proactivo transparente
  4. Forward a nvito-api /v1/mobile/events/:id con Authorization: Bearer <jwt>
  5. Si 401 → refresh → retry una vez → si falla → clear cookies → 401 al browser

3. Seguridad

Cookies encriptadas

CookieHttpOnlySecureSameSitePathMax-Age
__Host-nvito-atSiSiStrict/900 (15min)
__Host-nvito-rtSiSiStrict/api/auth2,592,000 (30d)
__Host-nvito-csrfNoSiStrict/2,592,000 (30d)
__Host-nvito-csrf-sigSiSiStrict/2,592,000 (30d)
  • Prefijo __Host-: Previene ataques de subdomain cookie injection
  • AES-256-GCM: Encriptacion de tokens con IV aleatorio de 12 bytes
  • CSRF doble submit: Token en cookie no-HttpOnly + firma en cookie HttpOnly, verificacion timing-safe

Headers de seguridad (CSP)

default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self';                    ← Bloquea llamadas directas a nvito-api
frame-src 'none';
frame-ancestors 'none';

Rate limiting

  • Login: 5 requests / IP / 15 minutos
  • Implementacion in-memory con limpieza periodica

Middleware

  • Rutas bajo (app)/* requieren cookie __Host-nvito-at
  • Rutas publicas: /login, /join/*, /api/auth/*, assets estaticos
  • Redireccion automatica a /login si no hay sesion

4. Estructura de archivos

nvito-pwa/src/
├── app/
│   ├── layout.tsx                      # Root: Providers wrapper
│   ├── providers.tsx                   # QueryClient + SessionProvider
│   ├── (auth)/
│   │   ├── layout.tsx                  # Auth layout con branding
│   │   ├── login/page.tsx              # Login HOST (eventCode + PIN)
│   │   └── join/[token]/page.tsx       # Login GUEST (accessToken URL)
│   ├── (app)/
│   │   ├── layout.tsx                  # App shell con auth check
│   │   ├── (host)/ (12 paginas)        # Flujo anfitrion
│   │   └── (guest)/ (8 paginas)        # Flujo invitado
│   └── api/
│       ├── auth/ (5 route handlers)    # Login, guest-access, refresh, logout, session
│       ├── proxy/[...path]/route.ts    # Catch-all proxy a nvito-api
│       └── push/subscribe/route.ts     # Web Push registration
├── lib/
│   ├── security/                       # AES-256-GCM, CSRF, rate limiter
│   ├── bff/                            # Cookie manager, proxy handler
│   ├── api/                            # Client, errors, services, types
│   ├── hooks/queries/                  # React Query hooks + keys
│   ├── validations/                    # Schemas Zod
│   ├── contexts/                       # Session, connectivity
│   ├── offline/                        # IndexedDB, offline queue
│   └── utils/                          # Constants, date, format
├── hooks/                              # use-online-status, use-install-prompt, etc.
├── components/shared/                  # Bottom nav, offline banner, etc.
├── middleware.ts                        # Auth guard
└── theme/colors.ts                     # Design tokens Nvito

5. Pantallas HOST (12)

PantallaTabRuta
DashboardTab 0/(host)/dashboard
InvitadosTab 1/(host)/guests
Scanner QRTab 2/(host)/scanner
MesasTab 3/(host)/tables
MasTab 4/(host)/more
MemoriasHidden/(host)/memorias
ItinerarioHidden/(host)/itinerary
Mesa de RegalosHidden/(host)/gift-registry
GaleriaHidden/(host)/gallery
Audio GuestbookHidden/(host)/audio-guestbook
MensajesHidden/(host)/messages
CompartirHidden/(host)/sharing

6. Pantallas GUEST (8)

PantallaTabRuta
InicioTab 0/(guest)/home
ProgramaTab 1/(guest)/itinerary-guest
InteractuarTab 2/(guest)/interact
MasTab 3/(guest)/more-guest
GaleriaHidden/(guest)/gallery-guest
CompartirHidden/(guest)/sharing-guest
Mesa de RegalosHidden/(guest)/gift-registry-guest
HospedajeHidden/(guest)/accommodation

7. Features nativas web

FeatureAPI WebFallback
QR Scannerhtml5-qrcode + getUserMediaInput manual de codigo
Audio RecordingMediaRecorder API
Photo Upload<input type="file" accept="image/*">
Sharingnavigator.share()Copiar al portapapeles
Haptic Feedbacknavigator.vibrate()Silencioso en iOS
OfflineService Worker + IndexedDB
Installbeforeinstallprompt eventBanner manual
Push NotificationsWeb Push API + VAPIDPolling (iOS sin PWA)

8. Soporte offline

  • IndexedDB: 3 stores (offline-queue, event-cache, qr-passes)
  • Cola FIFO: Solo operacion CHECK_IN se encola offline
  • Sincronizacion: Al reconectar, procesa cola y actualiza React Query cache
  • Banner visual: Indicador amarillo cuando no hay conexion

9. API Services y React Query

Servicios (6 modulos)

ServicioMetodosEndpoint base
events9/api/proxy/events/*
guests7/api/proxy/guests/*
media12/api/proxy/media/*
tables2/api/proxy/tables/*
payments2/api/proxy/payments/*
sharing3/api/proxy/sharing/*

Query Key Factory

Centralizado en lib/hooks/queries/keys.ts. Patron identico a nvito-client.

Polling

  • Stats del evento: cada 30 segundos (refetchInterval: 30_000)
  • QR passes: staleTime: 10min, gcTime: 7 dias (cache largo para offline)

10. Testing

CapaFrameworkSuitesTests
Security (crypto, CSRF, rate limiter, hardening)Vitest461
BFF (proxy handler)Vitest18
API (services, client, errors)Vitest343
Validations (auth schemas)Vitest115
Query KeysVitest18
Utils (date, format)Vitest226
Offline QueueVitest111
Config (env)Vitest19
MiddlewareVitest112
Hooks (online status, media recorder)Vitest27
Theme (colors)Vitest14
Total18204

Coverage thresholds: 80% statements, 70% branches, 75% functions, 80% lines

11. Diferencias clave vs nvito-client

Aspectonvito-clientnvito-pwa
PlataformaReact Native + ExpoNext.js 16 (browser)
DistribucionApp Store / Play StoreURL directa
Almacenamiento tokensSecureStore (encriptado nativo)Cookies HttpOnly AES-256-GCM
Tokens en JSSi (SecureStore accesible)No (patron BFF)
API callsDirectas a nvito-apiVia BFF proxy (/api/proxy/*)
Offline storageAsyncStorageIndexedDB
QR Scannerexpo-camerahtml5-qrcode + getUserMedia
Audioexpo-avMediaRecorder API
PushExpo Push NotificationsWeb Push API + VAPID
Hapticsexpo-hapticsnavigator.vibrate()

12. Variables de entorno

# Server-side only (NUNCA exponer al browser)
NVITO_API_URL=http://localhost:3000
COOKIE_ENCRYPTION_KEY=<64 hex chars>    # AES-256-GCM key
CSRF_SECRET=<64 hex chars>              # HMAC secret

# Web Push
VAPID_PUBLIC_KEY=<public key>
VAPID_PRIVATE_KEY=<private key>

# Public
NEXT_PUBLIC_APP_URL=http://localhost:3002

13. Desarrollo local

# Clonar y configurar
cd nvito-pwa
npm install
cp .env.local.example .env.local    # Editar variables

# Desarrollo
npm run dev                          # http://localhost:3002

# Testing
npm run test:run                     # Vitest
npm run build                        # Build de produccion

Prerequisito: nvito-api corriendo en :3000 (o la URL configurada en NVITO_API_URL).

Esta pagina fue util?