← Estado del proyectoKai Club · Docs

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

3. Modelo de disponibilidad (corazón del sistema)

Del póster real de tarifas se deduce el modelo exacto — dos tipos de inventario:

  1. 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).
  2. 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):

Anti doble-reserva:

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é

6. Seguridad

7. Despliegue

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