PWA que replica la funcionalidad de la app movil nativa (nvito-client), accesible via URL sin instalacion desde tiendas de aplicaciones.
| Campo | Valor |
|---|
| Version | 1.0 |
| Stack | Next.js 16.x, React 19, TypeScript 5, Tailwind v4 |
| Puerto local | 3002 |
| Dominio | app.nvito.mx |
| Estado | En desarrollo |
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.
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
- Usuario ingresa credenciales en el formulario de login
- Browser envía POST a
/api/auth/login (BFF route handler)
- BFF valida con Zod, aplica rate limiting, forward a
nvito-api /v1/mobile/auth/login
- BFF recibe JWT tokens de la respuesta
- BFF encripta tokens con AES-256-GCM y los almacena en cookies HttpOnly
- BFF retorna al browser solo metadata de sesion (role, eventId) — SIN tokens
- Browser →
GET /api/proxy/events/:id (con cookie HttpOnly automatica)
- BFF valida CSRF (si es mutacion), desencripta JWT de la cookie
- Si JWT expira en < 2 minutos → refresh proactivo transparente
- Forward a
nvito-api /v1/mobile/events/:id con Authorization: Bearer <jwt>
- Si 401 → refresh → retry una vez → si falla → clear cookies → 401 al browser
| Cookie | HttpOnly | Secure | SameSite | Path | Max-Age |
|---|
__Host-nvito-at | Si | Si | Strict | / | 900 (15min) |
__Host-nvito-rt | Si | Si | Strict | /api/auth | 2,592,000 (30d) |
__Host-nvito-csrf | No | Si | Strict | / | 2,592,000 (30d) |
__Host-nvito-csrf-sig | Si | Si | Strict | / | 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
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';
- Login: 5 requests / IP / 15 minutos
- Implementacion in-memory con limpieza periodica
- Rutas bajo
(app)/* requieren cookie __Host-nvito-at
- Rutas publicas:
/login, /join/*, /api/auth/*, assets estaticos
- Redireccion automatica a
/login si no hay sesion
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
| Pantalla | Tab | Ruta |
|---|
| Dashboard | Tab 0 | /(host)/dashboard |
| Invitados | Tab 1 | /(host)/guests |
| Scanner QR | Tab 2 | /(host)/scanner |
| Mesas | Tab 3 | /(host)/tables |
| Mas | Tab 4 | /(host)/more |
| Memorias | Hidden | /(host)/memorias |
| Itinerario | Hidden | /(host)/itinerary |
| Mesa de Regalos | Hidden | /(host)/gift-registry |
| Galeria | Hidden | /(host)/gallery |
| Audio Guestbook | Hidden | /(host)/audio-guestbook |
| Mensajes | Hidden | /(host)/messages |
| Compartir | Hidden | /(host)/sharing |
| Pantalla | Tab | Ruta |
|---|
| Inicio | Tab 0 | /(guest)/home |
| Programa | Tab 1 | /(guest)/itinerary-guest |
| Interactuar | Tab 2 | /(guest)/interact |
| Mas | Tab 3 | /(guest)/more-guest |
| Galeria | Hidden | /(guest)/gallery-guest |
| Compartir | Hidden | /(guest)/sharing-guest |
| Mesa de Regalos | Hidden | /(guest)/gift-registry-guest |
| Hospedaje | Hidden | /(guest)/accommodation |
| Feature | API Web | Fallback |
|---|
| QR Scanner | html5-qrcode + getUserMedia | Input manual de codigo |
| Audio Recording | MediaRecorder API | — |
| Photo Upload | <input type="file" accept="image/*"> | — |
| Sharing | navigator.share() | Copiar al portapapeles |
| Haptic Feedback | navigator.vibrate() | Silencioso en iOS |
| Offline | Service Worker + IndexedDB | — |
| Install | beforeinstallprompt event | Banner manual |
| Push Notifications | Web Push API + VAPID | Polling (iOS sin PWA) |
- 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
| Servicio | Metodos | Endpoint base |
|---|
| events | 9 | /api/proxy/events/* |
| guests | 7 | /api/proxy/guests/* |
| media | 12 | /api/proxy/media/* |
| tables | 2 | /api/proxy/tables/* |
| payments | 2 | /api/proxy/payments/* |
| sharing | 3 | /api/proxy/sharing/* |
Centralizado en lib/hooks/queries/keys.ts. Patron identico a nvito-client.
- Stats del evento: cada 30 segundos (
refetchInterval: 30_000)
- QR passes:
staleTime: 10min, gcTime: 7 dias (cache largo para offline)
| Capa | Framework | Suites | Tests |
|---|
| Security (crypto, CSRF, rate limiter, hardening) | Vitest | 4 | 61 |
| BFF (proxy handler) | Vitest | 1 | 8 |
| API (services, client, errors) | Vitest | 3 | 43 |
| Validations (auth schemas) | Vitest | 1 | 15 |
| Query Keys | Vitest | 1 | 8 |
| Utils (date, format) | Vitest | 2 | 26 |
| Offline Queue | Vitest | 1 | 11 |
| Config (env) | Vitest | 1 | 9 |
| Middleware | Vitest | 1 | 12 |
| Hooks (online status, media recorder) | Vitest | 2 | 7 |
| Theme (colors) | Vitest | 1 | 4 |
| Total | | 18 | 204 |
Coverage thresholds: 80% statements, 70% branches, 75% functions, 80% lines
| Aspecto | nvito-client | nvito-pwa |
|---|
| Plataforma | React Native + Expo | Next.js 16 (browser) |
| Distribucion | App Store / Play Store | URL directa |
| Almacenamiento tokens | SecureStore (encriptado nativo) | Cookies HttpOnly AES-256-GCM |
| Tokens en JS | Si (SecureStore accesible) | No (patron BFF) |
| API calls | Directas a nvito-api | Via BFF proxy (/api/proxy/*) |
| Offline storage | AsyncStorage | IndexedDB |
| QR Scanner | expo-camera | html5-qrcode + getUserMedia |
| Audio | expo-av | MediaRecorder API |
| Push | Expo Push Notifications | Web Push API + VAPID |
| Haptics | expo-haptics | navigator.vibrate() |
# 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
# 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).