ARCHITECTURE.md — KAI CLUB
Fecha: 2026-08-11 · Estado: aprobado para implementación (Fase 6 del proceso)
1. Decisión de stack (justificada, no por inercia)
| Capa | Elección | Por qué (y por qué no la alternativa) |
|---|---|---|
| Framework | Next.js 16 (App Router, RSC) + TypeScript | El problema nº1 del sitio actual es ser 100% client-side (Readymag: HTML vacío, LCP 19,8 s, contenido en PNG). SSR/SSG lo resuelve de raíz. App Router da i18n por segmento, streaming, next/image y OG dinámico. Alternativa Astro: excelente para lo público, pero el proyecto incluye motor de reservas + admin + Stripe → un solo framework full-stack reduce mantenimiento. |
| Estilos | Tailwind CSS v4 | Tokens de diseño como CSS vars, cero runtime, purga automática. Sin UI kits genéricos (evitar estética SaaS). |
| BD | PostgreSQL 16 + Prisma 6 | Las reservas exigen integridad transaccional (evitar doble reserva de un reservado de 600–900 €): constraints únicos parciales + transacciones. SQLite se descartó con motivo: escrituras concurrentes (público reservando + admin + cron) y bloqueo por filas. PG ya corre en srv1 → cero infraestructura nueva. Sin Docker en producción. |
| i18n | next-intl | Rutas /es /en /de, mensajes por locale, hreflang generado en layout. Contenido de BD con columnas JSON por locale ({"es":…,"en":…,"de":…}). |
| Auth | Auth.js v5 (credenciales) solo para /admin | No hay cuentas públicas en v1 (KAI PASS aplazado — ver PRODUCT_STRATEGY). RBAC: ADMIN, MANAGER, STAFF. Cookies httpOnly+secure+sameSite=lax, hash argon2. |
| Validación | Zod en todos los server actions / API | Nunca confiar en el cliente. |
| Pagos | Stripe (Payment Intents) — detrás de configuración | Tres modos por producto: SIN_PAGO (default de lanzamiento), DEPOSITO, PAGO_TOTAL. Webhook con verificación de firma + reconciliador cron cada 5 min contra Stripe (lección kellymarlow: jamás depender solo del webhook). |
Nodemailer SMTP + tabla mail_outbox |
El SMTP del cliente llegará tarde (pasa en todos los proyectos). La app funciona sin SMTP desde el día 1: los correos se encolan en mail_outbox y un cron los despacha cuando haya credenciales. Nada se pierde. |
|
| Media | Pipeline sharp (AVIF/WebP + srcset) y ffmpeg para vídeo (poster + MP4 H.264 móvil ≤2,5 MB + desktop ≤6 MB) | El sitio actual carga 199 MB. Presupuesto duro de página: ver PERFORMANCE en este doc. |
| Cron | node-cron in-process (pm2) | Recordatorios, reconciliación Stripe, despacho de outbox, refresh clima/sunset. Sin Redis ni colas: volumen no lo justifica. |
| Tests | Vitest (unidad: disponibilidad, precios) + Playwright (E2E: flujo de reserva móvil, admin) | Igual que Jornada/Rías Bajas. |
2. Decisión crítica: motor de reservas propio vs CoverManager
Hoy Kai usa CoverManager (enlace externo) para mesas de restaurante, y teléfono/WhatsApp para experiencias y reservados.
Decisión v1 (pragmática, no dogmática):
- Motor PROPIO para EXPERIENCIAS y RESERVADOS (hamacas 1ª/2ª-3ª línea, Keola/Kamala/Kaikane/Aloha/Makai). Hoy NO tienen ningún canal online — es el mayor hueco comercial (productos de 60–900 € que solo se venden por teléfono). Aquí está todo el valor nuevo: calendario, cupos, extras, depósito.
- CoverManager SE MANTIENE para mesa de restaurante en v1, integrado como paso del flujo («¿Mesa para comer? → CoverManager» con UTM y locale correcto). Motivo: el personal ya lo opera, gestiona no-shows, y romper la operativa de sala en pleno lanzamiento es riesgo sin recompensa. Migración a motor propio: evaluar en v2 cuando el motor propio esté rodado (ahorro de cuota + datos unificados).
3. Modelo de disponibilidad (corazón del sistema)
Del póster real de tarifas se deduce el modelo exacto — dos tipos de inventario:
- POOLED (por cupo diario): Experiencia Kai 1ª línea (N hamacas king), 2ª-3ª línea (M hamacas). No se elige hamaca concreta; se reserva plaza en el tier. Capacidad configurable por producto y por fecha (overrides).
- UNIQUE (unidad nominal): los 5 reservados (Keola, Kamala, Kaikane, Aloha, Makai). Un booking por unidad y día.
Reglas de negocio codificadas (de las tarifas reales):
- Experiencias válidas sáb/dom/festivos; L–V rige consumo mínimo 40 €/persona (el motor muestra el producto correcto según el día; festivos canarios en tabla
calendar_overrides). - Cierre martes y miércoles (no reservable, configurable).
- Precio =
acceso × pax+ consumo mínimo informativo (no se cobra online, se muestra SIEMPRE desglosado). - Niños 4–12: producto infantil ligado (5 € + 20 €).
Anti doble-reserva:
- UNIQUE: índice único parcial
(product_id, date) WHERE status IN ('PENDING','CONFIRMED'). La inserción concurrente pierde y recibe «acaba de ocuparse». - POOLED: transacción
SERIALIZABLE(oSELECT … FOR UPDATEdel contador del día) que comparaSUM(pax)contra capacidad antes de insertar. PENDINGcaduca a los 20 min (cron) para no bloquear inventario con carritos abandonados.
4. Estructura del repo
kai-club/
├─ app/
│ ├─ [locale]/ # es | en | de (next-intl)
│ │ ├─ (public)/
│ │ │ ├─ page.tsx # Home narrativa
│ │ │ ├─ experiencias/ # + [slug]
│ │ │ ├─ reservar/ # wizard 4 pasos
│ │ │ ├─ carta/ bebidas/
│ │ │ ├─ eventos/ # + [slug]
│ │ │ ├─ celebrar/ # eventos privados (lead wizard)
│ │ │ ├─ galeria/ contacto/ faq/
│ │ │ └─ legal/ privacidad/ cookies/
│ │ └─ admin/ # protegido (Auth.js + RBAC)
│ └─ api/
│ ├─ webhooks/stripe/route.ts
│ └─ cron/route.ts # protegido por token
├─ components/ (ui/ sections/ booking/ admin/)
├─ lib/ (db, auth, availability, pricing, stripe, mail, weather, i18n)
├─ prisma/schema.prisma
├─ messages/{es,en,de}.json
├─ public/media/{pool,food,drinks,events,people,sunset,venue}
└─ scripts/ (optimize-media.mjs, seed.ts)
Reglas: componentes < 200 líneas, lógica de negocio en lib/ (testeable), server actions solo orquestan, cero any.
5. Rendering y caché
- Páginas públicas: SSG/ISR (revalidate 60–300 s; carta y eventos con
revalidateTagal guardar en admin). HTML completo con todo el contenido indexable — lo contrario exacto de Readymag. KAI TODAY: server component con datos de BD + clima (Open-Meteo, sin API key) + sunset (cálculo astronómico local, sin API) cacheados 30 min en servidor. Nunca bloquea el LCP (streaming/Suspense).- Wizard de reserva y admin: dinámicos.
- Presupuesto de página (móvil, home): HTML+CSS+JS < 300 KB, imágenes above-the-fold < 200 KB, vídeo hero móvil ≤ 2,5 MB con poster inmediato y
preload="none"hasta interacción/idle. LCP objetivo < 2,5 s en 4G.
6. Seguridad
- RBAC en cada server action de admin (no solo en middleware).
- Zod en toda entrada; sanitización de HTML de contenido admin (sin HTML libre: campos estructurados).
- Rate limiting: nginx
limit_req+ contador in-memory en actions sensibles (reservas, leads, login). - CSRF: server actions de Next (origin-check) + sameSite; login con protección de fuerza bruta (backoff).
- Cookies seguras, secrets en
.envfuera del repo,audit_logsde acciones sensibles (cambios de tarifas, cancelaciones, exports). - Cabeceras: CSP sin CDNs externos (todo self-hosted, fuentes incluidas), HSTS, X-Content-Type-Options, Referrer-Policy.
- RGPD: consentimiento explícito en formularios, política de privacidad/cookies reales (hoy el sitio NO las tiene), minimización de datos (ver DATABASE_SCHEMA), borrado/export de cliente en admin.
7. Despliegue
- srv1 (patrón probado: Rías Bajas, Apartamentos):
next buildconoutput:'standalone', pm2kai-clubpuerto :4700, nginx vhost + certbot. - Demo:
kai.rcsolucionesdigitales.com→ cuando el cliente apruebe,kaiclub.esapunta a srv1 con redirects 301 de las 8 URLs antiguas (mapa en SEO_STRATEGY). - Backup BD: pg_dump diario (cron srv1, mismo esquema que resto de proyectos).
- CI local:
lint + typecheck + vitest + buildantes de cada deploy (deploy/update-app.shestilo Rías Bajas).
8. Integraciones y dependencias externas (todas con fallback)
| Servicio | Uso | Fallback si falta |
|---|---|---|
| CoverManager | Mesa restaurante (enlace embebido) | Enlace directo + WhatsApp |
| Stripe | Depósitos reservados (flag) | Modo SIN_PAGO (default lanzamiento) |
| SMTP cliente | Confirmaciones/avisos | mail_outbox encola; WhatsApp manual |
| Open-Meteo | Clima KAI TODAY | Se oculta el dato (nunca inventar) |
| GA4 (+Consent Mode v2) | Analytics | Sitio funciona igual sin consentimiento |
| #KAIMOMENTS | Contenido curado manual desde admin (sin API frágil) |
9. Lo que explícitamente NO lleva esta arquitectura
Sin chatbot/IA conversacional (prohibido por brief). Sin Redis/colas. Sin microservicios. Sin Docker en producción. Sin CMS headless externo (el admin ES el CMS). Sin librería de componentes pesada. Sin cuentas de usuario públicas en v1. Sin sincronización automática de reseñas (curación manual en admin — fiabilidad > integración frágil).