Documentación técnica.
Cómo está construida esta demo y cómo comprobar por su cuenta cada capacidad del stack.
Resumen
Sitio de demostración de Northlav Studio con un catálogo ficticio de equipos de construcción. Está en producción con el mismo stack de un proyecto headless real:
- Next.js 15 con App Router y React Server Components.
- Payload CMS 3 autoalojado en el mismo proyecto.
- PostgreSQL en Google Cloud SQL.
- Imágenes y PDF en Google Cloud Storage.
- Tailwind CSS y shadcn/ui.
- TypeScript en modo estricto con pnpm, desplegado en Vercel.
Demuestra la conexión serverless a la base, la persistencia de archivos, la actualización del sitio sin volver a desplegar, las especificaciones técnicas estructuradas y un formulario listo para integraciones.
Cómo probarlo
Con el usuario de prueba (rol editor) en el panel:
- Archivos: en /admin → Media, suba una imagen o un PDF. Queda en Cloud Storage y se sirve desde el sitio.
- Actualización al guardar: edite el resumen de un producto, guarde y recargue su ficha. El cambio aparece sin volver a desplegar.
- Especificaciones: abra la Excavadora EX-45: cada especificación es un dato con tipo, valor y unidad.
- Cotización: envíe una solicitud desde /cotizar con datos ficticios y véala en el panel → Cotizaciones.
- Conexión: todo lo anterior ocurre en funciones de Vercel que leen y escriben en Cloud SQL (sección siguiente).
Infraestructura
Navegador
│
▼
Vercel · funciones Node en iad1 (Washington D. C.)
├──► Cloud SQL · PostgreSQL 16 en us-east4
│ conector oficial de Cloud SQL, TLS con certificado de cliente efímero
│
└──► Cloud Storage · bucket privado en us-east4
los archivos se sirven a través de Payload- Conexión:
@google-cloud/cloud-sql-connectorcon IP pública sin redes autorizadas. El conector obtiene un certificado efímero con la identidad de Google y abre un túnel TLS verificado; la instancia exige certificados de cliente. - Pool por instancia de función: máximo 3 conexiones (
DB_POOL_MAX), 10 s de inactividad y 10 s de espera para conectar. El total posible esDB_POOL_MAX × instancias activas, y se contrasta contra elmax_connectionsde la instancia. - Regiones: Vercel en
iad1y Cloud SQL enus-east4, geográficamente cercanas para reducir latencia. - Build:
next buildno necesita la base. Las migraciones se aplican a mano, nunca durante el despliegue.
Modelo de datos
Colecciones de Payload: Productos, Categorías, Cotizaciones, Media, Usuarios y Redirecciones.
Las especificaciones técnicas no son texto libre: son filas estructuradas que permiten tablas consistentes, comparar equipos, filtrar por valor y exportar a otros sistemas.
especificaciones: [
{ clave: "potencia", etiqueta: "Potencia", tipo: "numero", valorNumero: 45, unidad: "kW", destacada: true },
{ clave: "peso-operativo", etiqueta: "Peso operativo", tipo: "numero", valorNumero: 5200, unidad: "kg", destacada: true },
{ clave: "motor", etiqueta: "Motor", tipo: "texto", valorTexto: "Motor diésel de demostración" }
]tipo: "numero"exige un número finito y admite unidad;tipo: "texto"exige texto. El valor que no corresponde se limpia al guardar.- Las claves son únicas por producto.
- Variantes con códigos únicos y tabla de medidas ligada a variantes existentes. La cotización valida que la variante exista y esté disponible.
Actualización al guardar
- Un editor guarda o elimina un producto, una categoría, un archivo o una redirección.
- Payload ejecuta los hooks
afterChangeoafterDelete. - El hook llama a
revalidateTagcon la etiqueta del contenido (productos, categorías o redirecciones). - Las consultas públicas están cacheadas con esas etiquetas. La siguiente visita ya no usa la caché y lee los datos nuevos de Cloud SQL.
Sin cambios, las páginas no consultan la base. No hace falta volver a desplegar ni un endpoint manual de revalidación.
Archivos
- Imágenes (JPG, PNG, WebP) y PDF se suben desde el panel con el adaptador oficial
@payloadcms/storage-gcs. - Payload genera tres tamaños de cada imagen: 400, 768 y 1600 px.
- El bucket es privado: los archivos se sirven a través de Payload (
/api/media/file/…), así que sus permisos se respetan. - En las páginas,
next/imageentrega WebP o AVIF del tamaño justo para cada pantalla. - Los archivos persisten entre despliegues: no viven en el sistema de archivos de la función.
Cotizaciones e integraciones
El formulario de /cotizar y el endpoint POST /api/cotizaciones usan el mismo servicio: validación, comprobación de producto y variante, persistencia e idempotencia.
POST /api/cotizaciones
Content-Type: application/json
{
"nombre": "Ana", "apellido": "Solís",
"telefono": "88887777",
"correo": "ana@ejemplo.com",
"producto": "generador-gd-60",
"variante": "GD-60",
"mensaje": "Para dos semanas",
"idempotencyKey": "b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}| Código | Significado |
|---|---|
| 201 | Solicitud creada. Devuelve id y eventoId. |
| 200 | Reintento con la misma clave y el mismo contenido: devuelve la solicitud existente, sin duplicar. |
| 400 | Campos inválidos, producto o variante no disponibles, JSON mal formado o control antispam. |
| 403 | Origen de navegador no permitido. |
| 409 | La clave de idempotencia ya se usó con otro contenido. |
| 413 / 415 | Cuerpo mayor de 16 KB o tipo de contenido distinto de JSON. |
| 429 | Límite de envíos alcanzado. Incluye Retry-After. |
| 500 | No se guardó. Se puede reintentar con la misma clave. |
Punto de salida para integraciones. Después de guardar, el servicio llama a notificarSolicitud() en src/lib/outbound.ts con un contrato tipado y versionado. Para conectar Brevo, Pub/Sub o una API de conversiones se implementa un adaptador:
export const adaptadorPubSub: AdaptadorSalida = {
nombre: 'pubsub',
async enviar(solicitud) {
// solicitud: { version, solicitudId, eventoId, creadaEn, datos }
await topic.publishMessage({ json: solicitud })
return { ok: true }
},
}
usarAdaptador(adaptadorPubSub)Un fallo del adaptador no borra la solicitud ni la duplica. En esta demo el adaptador no envía a terceros: solo registra el ID y el resultado.
Medición
- Aviso de consentimiento con Aceptar y Rechazar. Rechazar no impide enviar la cotización.
- Con consentimiento y solo después de guardar, se envía al
dataLayer:
{ "event": "solicitud_cotizacion", "evento_id": "…", "producto": "generador-gd-60", "variante": "GD-60" }evento_ides el mismo que se guarda con la solicitud: sirve para deduplicar conversiones entre el navegador y el servidor.- Un reintento no repite el evento. Nunca incluye nombre, teléfono, correo ni mensaje.
- No hay identificadores de terceros escritos en el código.
SEO
- Todo el contenido, los filtros y la búsqueda se renderizan en el servidor y funcionan sin JavaScript.
- Título, descripción e imagen para redes editables desde el CMS.
- JSON-LD generado desde el CMS:
Productcon cada especificación comoPropertyValuecon su unidad,BreadcrumbList,ItemList,OrganizationyWebSite. - sitemap.xml con solo contenido publicado y robots.txt.
- Redirecciones 301 administrables desde el panel para el mapa del sitio anterior: un solo salto al destino, sin distinguir mayúsculas ni barra final.
Seguridad y permisos
| Colección | Público | Editor | Administrador |
|---|---|---|---|
| Productos | Solo publicados | Crea y edita, sin eliminar | Todo |
| Categorías | Lectura | Crea y edita, sin eliminar | Todo |
| Media | Lectura | Sube y edita, sin eliminar | Todo |
| Cotizaciones | Sin acceso | Solo lectura | Cambia estado y elimina; datos del cliente de solo lectura |
| Usuarios | Sin acceso ni registro | Solo su propio usuario, sin cambiar rol | Todo |
| Redirecciones | Lectura | Sin acceso | Todo |
- Los permisos se aplican en REST, GraphQL y en las consultas del servidor, no solo en el panel.
- Cabeceras:
Content-Security-Policyconframe-ancestors,X-Frame-Options,Referrer-Policy,X-Content-Type-Optionsy HSTS. - Secretos solo en variables de entorno del servidor. El repositorio no contiene credenciales.
- Formulario con campo trampa, tiempo mínimo de llenado, límite por teléfono y por IP, y validación de origen.
En el proyecto real
Esta demo funciona en un entorno simplificado en cuentas de Northlav, con datos ficticios. En un proyecto de cliente:
- Toda la infraestructura se crea en las cuentas del cliente.
- Entornos separados de desarrollo, previsualización y producción, con base y bucket propios.
- Autenticación con Google Cloud por federación de identidades, sin llaves de cuenta de servicio.
- Usuarios de base de datos con privilegios mínimos.
- Límite de envíos global con un almacén compartido.
Video de infraestructura
Recorrido de 3 minutos: la instancia de Cloud SQL, la subida de archivos al bucket, la actualización al guardar y una cotización que llega al panel. La carpeta incluye también los reportes Lighthouse.
Abrir la carpeta con el video ↗