home
  • Español
  • English
  • Português
  • Cómo trabajo
  • star Proyectos Destacados
  • Experiencia
  • mail Contacto
arrow_back Volver al proyecto Inicio

Documentación del proyecto

Documentación · Conciergo

11 archivos markdown (producto, arquitectura, API, auth, datos, deploy…).

En esta página Visión general 0% expand_more
Visión general Enlaces Stack En cifras Historia Cómo leer esta documentación Producto y features Problema Solución Quién la usa Pantallas Landing (/) Acceso (/auth/sign-in, /auth/sign-up) Eventos (/events) Mapa (/events/map) Nuevo bolo (/events/new) Ficha de un bolo (/events/:id) Conexiones (/matches) Chat (/matches/:matchId) Perfil (/profile) y perfil de otra persona (/profile/:userId) Reglas Arquitectura Vista general Cómo viaja una petición Dos formas de arrancar la API Cómo llega un mensaje Estado en el cliente Base de datos Carga del front Estructura del workspace Comandos Ajustes del workspace Superficie API Salud Usuarios Eventos Conexiones y mensajes Chat en vivo (Socket.io) La regla de las conexiones Errores Autenticación Neon Auth Del login al usuario de la base de datos El nombre de una cuenta nueva Invitados El chat Al cerrar sesión o cambiar de cuenta Configuración Modelo de datos users events event_attendees matches messages Cascadas Fuera del esquema Cómo se aplica el esquema Lo que devuelve la API Deploy Vercel Qué hace Vercel con main Variables de entorno Lo que no hay en Vercel En local Decisiones de producto y tech Apuntarse es el match Borrarse quita el chat El cartel es público El contrato manda Neon Auth en lugar de Clerk La app Express separada del servidor Base de datos perezosa Instalaciones con un día de margen Solo main despliega Retos y resultados Retos Salir de Replit Sacar un JWT de Neon Auth Cuentas sin nombre Google Maps Conexiones que sobrevivían Lo que falta Resultados Front · React Rutas Datos Formularios Interfaz Carga

01-overview.md

Visión general · Conciergo

Conciergo es una web mobile-first para encontrar con quién ir a un concierto. Cada bolo tiene su ficha y su marcador en el mapa. Al apuntarte, la app te conecta con cada persona que también va y os abre un chat para quedar antes. No hay swipes ni perfiles que puntuar: ir al mismo concierto es la única condición.

El cartel y el mapa se ven sin cuenta. Apuntarse, añadir un bolo, ver tus conexiones, chatear y editar el perfil piden sesión con Neon Auth (email y contraseña, o Google).

El repo se llama Concert-Connect; la marca es Conciergo (en la interfaz, ConcierGo). Es un workspace pnpm con un front React + Vite, una API Express y un contrato OpenAPI del que se generan los hooks del cliente y los esquemas Zod.

Enlaces

  • Web: conciergo-sigma.vercel.app
  • GitHub: Criscode2022/Concert-Connect
  • Salud de la API: /api/healthz responde {"status":"ok"}

Stack

CapaTecnología
WebReact 19.1, Vite 7, Tailwind CSS 4, componentes shadcn/ui (Radix), Wouter 3, TanStack Query 5, Framer Motion
ContratoOpenAPI 3.1 (lib/api-spec/openapi.yaml); Orval genera los hooks de React Query y los esquemas Zod
APIExpress 5 en Node, logs con pino; Socket.io para el chat cuando la API corre como servidor
AuthNeon Auth (Better Auth). La API verifica el JWT con jose contra el JWKS de Neon Auth
DatosNeon Postgres en Frankfurt (eu-central-1) con Drizzle ORM sobre pg
MapaGoogle Maps JavaScript API con marcadores avanzados
DeployVercel: el front como estáticos y la API como una sola función
Monorepopnpm 11 con workspaces y catálogo de versiones, TypeScript 5.9, Node 22 o superior

En cifras

Rutas del front10: landing, acceso, eventos, mapa, nuevo bolo, ficha, conexiones, chat, perfil y perfil de otra persona
Operaciones de la API15 en el contrato OpenAPI
Tablas5: users, events, event_attendees, matches, messages
Bolos en producción6, en Madrid y Barcelona, uno por género (20 de octubre de 2026)
TestsNinguno automatizado. Ver Testing · visión

Historia

El proyecto nació en Replit en junio de 2026 con Clerk como login. En agosto pasó a Vercel y Neon Auth (Replace Clerk and Replit with Neon Auth and Vercel), y en septiembre la base de datos se movió a un proyecto Neon en Frankfurt. Quedan restos de Replit que no se despliegan: artifacts/mockup-sandbox, attached_assets/ y scripts/post-merge.sh.

Cómo leer esta documentación

DocumentoPara
Producto y featuresQué hace la app, pantalla por pantalla
ArquitecturaCómo viaja una petición y cómo llega un mensaje
Estructura del workspaceQué hay en cada paquete y qué comandos existen
Superficie APILas 15 operaciones y el handshake del chat
AutenticaciónDel login de Neon Auth al usuario de la base de datos
Modelo de datosTablas, borrados en cascada y cómo se crea el esquema
Deploy en VercelCómo llega main a producción y cómo se arranca local
DecisionesPor qué está hecho así
Retos y resultadosLo que costó, lo que funciona y lo que falta
Front · ReactLas prácticas del cliente React

La parte de testing empieza en Testing · visión.

02-product.md

Producto y features

Problema

Un concierto al que te apetece ir se queda en nada si nadie de tu entorno se apunta. Los grupos de mensajería no conectan a desconocidos que tienen el mismo plan, y las apps de citas no van de música.

Solución

En Conciergo, apuntarse a un bolo es el gesto que conecta: la app te empareja con cada persona que también va y os abre un chat para quedar antes, compartir el viaje o guardar sitio delante.

Quién la usa

  • Sin cuenta: mira el cartel, el mapa y la ficha de cada bolo. El botón de apuntarse le pide iniciar sesión.
  • Con cuenta: se apunta y se borra de bolos, añade bolos nuevos, chatea con sus conexiones y edita su perfil.

La interfaz está en español, y las fechas también (date-fns con locale es).

Pantallas

Landing (/)

Titular de cartel, «No dejes que un gran bolo se pierda», y tres salidas: Entrar, Unirse y Explorar eventos. Con la sesión abierta, redirige a /events.

Acceso (/auth/sign-in, /auth/sign-up)

Las vistas de @neondatabase/auth-ui con el estilo de la app: email y contraseña o Google, nombre obligatorio al registrarse y recuperación de contraseña. Al entrar, lleva a /events. /sign-in y /sign-up redirigen aquí.

Eventos (/events)

Los bolos ordenados por fecha descendente, el más tardío primero (la API devuelve hasta 50). Cada tarjeta enseña artista, recinto, fecha y asistentes, con el botón de apuntarse. Encima, chips de género construidos con los géneros de los bolos cargados; tocar uno filtra la lista, y tocar el género de una tarjeta hace lo mismo. Mientras llegan los datos y las imágenes se ve un esqueleto, para que la lista no salte. Con sesión, un botón flotante lleva a añadir un bolo.

Mapa (/events/map)

Google Maps centrado en Madrid con zoom de país, un marcador por bolo. Al tocar uno se abre su ficha resumida con el botón de apuntarse o cancelar. Si falta la clave de Maps, la página lo dice en lugar de romperse.

Nuevo bolo (/events/new)

Formulario con validación Zod: título, artista, recinto y fecha obligatorios; género y URL de imagen opcionales. Quien crea el bolo queda apuntado. El formulario no pide la ubicación y envía lat: 0 y lng: 0, así que un bolo creado desde la app no sale en el mapa de España (ver Retos y resultados).

Ficha de un bolo (/events/:id)

Datos del bolo, número de asistentes y el botón Apuntarme al bolo / Apuntado · Cancelar. Al apuntarte aparece el bloque Tus Compañeros. Sin sesión, el botón dice Inicia sesión para apuntarte.

Conexiones (/matches)

Una fila por persona y bolo, con su avatar, su nombre, el artista, el último mensaje con su hora y un contador de mensajes recibidos. La lista se vuelve a pedir cada 20 segundos y la suma de los contadores sale como badge en la navegación. Sin conexiones, la pantalla dice «El backstage está vacío».

Chat (/matches/:matchId)

Cabecera con el bolo, mensajes con su hora y un campo que envía con Intro. Un chat vacío invita a romper el hielo.

Perfil (/profile) y perfil de otra persona (/profile/:userId)

El perfil propio edita nombre, bio, ciudad y géneros favoritos (16 para elegir); el avatar viene de la cuenta de Neon Auth, y desde aquí se cierra la sesión. El de otra persona, Backstage de…, enseña su avatar, ciudad, bio y géneros.

Reglas

  • Apuntarse crea las conexiones: una por cada otra persona apuntada al mismo bolo. Una pareja tiene una sola conexión por bolo.
  • Borrarse las quita: al dejar de ir, se borran tus conexiones de ese bolo y, con ellas, sus mensajes. Solo chatea quien sigue apuntado.
  • El chat es privado: solo las dos personas de una conexión pueden leerlo o escribir en él.
  • Navegación: barra lateral en escritorio y barra inferior en el móvil. Sin sesión solo se ven Eventos y Mapa.

03-architecture.md

Arquitectura

Vista general

Navegador · SPA React (Vite)
  │  sesión de Neon Auth (cookie) ──► Neon Auth: login, Google, JWT
  │  hooks de Orval + Bearer JWT
  ▼
Vercel
  ├─ estáticos: artifacts/conciergo/dist/public
  └─ /api/* ──► función api/index.js ──► app Express (dist/app.mjs)
                    │  requireAuth / optionalAuth: JWT verificado con el JWKS
                    ▼
               Neon Postgres (Drizzle + pg)
               users · events · event_attendees · matches · messages

El mapa carga la Google Maps JavaScript API directamente desde el navegador.

Cómo viaja una petición

  1. Hook generado. Una página llama a un hook de @workspace/api-client-react, por ejemplo useAttendEvent(). Orval lo genera desde openapi.yaml.
  2. Token. El customFetch del paquete pide el token a getAccessToken() (registrado con setAuthTokenGetter) y añade Authorization: Bearer <jwt>. Sin sesión, la petición sale sin cabecera.
  3. Vercel. La regla /api/:path* → /api/index manda la petición a la función, que importa la app Express ya compilada y la reutiliza entre peticiones.
  4. Middleware. requireAuth verifica el JWT y responde 401 si falta o no vale; optionalAuth deja pasar a los invitados. Los dos crean el usuario en la tabla users la primera vez que lo ven (ver Autenticación).
  5. Ruta. La ruta consulta Drizzle y da forma a la respuesta con funciones como eventToJson o userToJson: fechas en ISO, nulos explícitos y campos calculados (attendeesCount, isAttending, unreadCount).
  6. Caché. TanStack Query guarda la respuesta. Tras apuntarse o borrarse, invalidateAfterAttendance() invalida eventos y conexiones para que la siguiente pantalla no enseñe datos viejos.

Dos formas de arrancar la API

artifacts/api-server/build.mjs compila con esbuild dos entradas:

FicheroQué esDónde se usa
dist/app.mjsSolo la app Express, sin listenLa función de Vercel (api/index.js)
dist/index.mjsServidor HTTP en PORT (8080) con Socket.ioEn local, con pnpm --filter @workspace/api-server run dev

Separar la app del servidor es lo que permite meter Express en una función. El precio es el chat en vivo: Socket.io necesita un servidor que siga abierto, y en Vercel no lo hay.

Cómo llega un mensaje

  1. El cliente envía POST /api/matches/:matchId/messages.
  2. La API comprueba que la persona pertenece a la conexión, guarda el mensaje y llama a broadcastMessage().
  3. En local, Socket.io emite new_message a la sala chat:<matchId> y el otro navegador lo añade a la caché al instante.
  4. En Vercel no hay servidor de Socket.io (/api/socket.io responde 404), así que broadcastMessage() no hace nada. El otro lado ve el mensaje cuando su caché se refresca: al volver a la pestaña, al reconectar o al abrir el chat. La lista de conexiones se vuelve a pedir cada 20 segundos.

Estado en el cliente

  • Sesión: la gestiona el cliente de Neon Auth (authClient.useSession()).
  • Datos: todo pasa por TanStack Query. Por defecto, los datos valen 15 segundos, se refrescan al volver a la ventana y al reconectar, un 401 se reintenta hasta tres veces (el token puede estar llegando) y el resto de errores 4xx no se reintenta.
  • Cambio de cuenta: AuthQueryClientCacheInvalidator vacía la caché cuando una sesión abierta se cierra o cambia de usuario, para que nadie vea las conexiones de la cuenta anterior.

Base de datos

lib/db exporta db como un Proxy: el pool de pg (máximo 5 conexiones) se crea en la primera consulta. Así importar el paquete no exige DATABASE_URL, y una función de Vercel que solo responde /api/healthz no abre conexiones.

Carga del front

Cada página es un lazy() con su Suspense, y Vite separa en trozos propios React, TanStack Query y Framer Motion. La primera visita a la landing no descarga el mapa, el chat ni el formulario.

04-monorepo.md

Estructura del workspace

Un workspace pnpm (pnpm-workspace.yaml) con aplicaciones en artifacts/ y librerías en lib/. Los nombres de paquete empiezan por @workspace/.

RutaPaqueteQué es
artifacts/conciergo@workspace/conciergoLa app React + Vite. Páginas en src/pages, layout en src/components/layout
artifacts/api-server@workspace/api-serverLa API Express: rutas, middlewares de auth, Socket.io y el build con esbuild
api/index.js—La función de Vercel: importa artifacts/api-server/dist/app.mjs
lib/api-spec@workspace/api-specopenapi.yaml y la configuración de Orval
lib/api-client-react@workspace/api-client-reactHooks de React Query generados, más custom-fetch.ts (base /api, token, errores)
lib/api-zod@workspace/api-zodEsquemas Zod generados desde el mismo contrato
lib/db@workspace/dbEsquema Drizzle (src/schema/), el cliente db y drizzle.config.ts
artifacts/mockup-sandbox@workspace/mockup-sandboxSandbox de Replit para previsualizar componentes. No se despliega
scripts@workspace/scriptshello.ts de ejemplo y post-merge.sh (instalar y aplicar el esquema tras un merge en Replit)
attached_assets/—Imágenes adjuntadas en Replit, con alias @assets en Vite; ninguna página las importa
.agents/memory/—Notas para agentes: decisiones del stack y sistema de diseño
neon.ts—Configuración de Neon para el proyecto: Neon Auth activado
docs/—Esta documentación

Comandos

ComandoQué hace
pnpm installInstala todo. Un preinstall aborta si se usa npm o yarn
pnpm --filter @workspace/api-server run devCompila la API y la arranca en :8080 con Socket.io
pnpm --filter @workspace/conciergo run devVite en :5173, con /api redirigido a la API local
pnpm run typechecktsc --build de las librerías y tsc --noEmit de cada app y de scripts
pnpm run buildEl typecheck y después el build de cada paquete que lo tenga
pnpm --filter @workspace/api-spec run codegenRegenera hooks y esquemas desde openapi.yaml y comprueba los tipos de las librerías
pnpm --filter @workspace/db run pushAplica el esquema Drizzle a la base de DATABASE_URL (drizzle-kit push)

pnpm run build falla hoy porque el typecheck de la web da 8 errores (ver Testing · integración). Vercel no lo usa: su build llama directamente a los builds de la web y de la API.

Ajustes del workspace

  • Catálogo de versiones. React, Vite, Tailwind, TanStack Query, Drizzle, Zod y el resto de dependencias compartidas se declaran una vez en catalog: y los paquetes las piden con "catalog:".
  • Antigüedad mínima. minimumReleaseAge: 1440 impide instalar una versión publicada hace menos de un día, como defensa ante paquetes comprometidos. @neondatabase/* y jose están exentos.
  • Builds permitidos. Solo esbuild, @swc/core, msw y unrs-resolver pueden ejecutar scripts de instalación, y esbuild está fijado a 0.27.3 para todo el árbol.
  • Versiones de herramientas. packageManager: pnpm@11.21.0, engines.node >= 22, TypeScript ~5.9.3.

05-api.md

Superficie API

Base: /api. El contrato es lib/api-spec/openapi.yaml (OpenAPI 3.1, 15 operaciones); la implementación vive en artifacts/api-server/src/routes/.

Acceso: pública no mira el token; opcional lo usa si llega (para calcular isAttending); sesión responde 401 sin un JWT válido.

Salud

MétodoRutaAccesoRespuesta
GET/healthzpública{ "status": "ok" }

Usuarios

MétodoRutaAccesoQué hace
GET/users/mesesiónEl perfil propio
PUT/users/mesesiónCambia solo los campos enviados: name, bio, avatarUrl, musicGenres, city
GET/users/:userIdsesiónEl perfil de otra persona; 404 si no existe

Eventos

MétodoRutaAccesoQué hace
GET/eventsopcionalHasta 50 bolos por fecha descendente; ?genre= filtra por género exacto
POST/eventssesiónCrea un bolo y apunta a quien lo crea. 201
GET/events/:eventIdopcionalUn bolo; 404 si no existe
DELETE/events/:eventIdsesiónBorra un bolo propio (403 si es de otra persona). La interfaz no lo usa
POST/events/:eventId/attendsesiónApunta y crea una conexión con cada otra persona apuntada
DELETE/events/:eventId/attendsesiónBorra la asistencia y las conexiones de ese bolo, con sus mensajes. 204
GET/events/statssesiónTotales de bolos, personas y bolos futuros, y los cinco géneros con más bolos. La interfaz no lo usa

Cada bolo se devuelve con createdBy (el perfil de quien lo creó), attendeesCount e isAttending.

Conexiones y mensajes

MétodoRutaAccesoQué hace
GET/matchessesiónTus conexiones, la más nueva primero, con otherUser, event, lastMessage y unreadCount
GET/matches/:matchIdsesiónUna conexión; 404 si no existe y 403 si no es tuya
GET/matches/:matchId/messagessesiónEl historial en orden cronológico; cada mensaje lleva senderAuthId
POST/matches/:matchId/messagessesiónEnvía un mensaje (400 si está vacío). 201, y lo emite por Socket.io si hay servidor

unreadCount cuenta todos los mensajes que te ha enviado la otra persona: no se guarda qué has leído.

Chat en vivo (Socket.io)

Ruta: /api/socket.io. Solo existe cuando la API corre como servidor (en local); en Vercel responde 404 (ver Arquitectura).

El cliente se conecta con { token, matchId } en el handshake. El servidor verifica el JWT, busca al usuario y comprueba que la conexión es suya; si algo falla, rechaza el socket con Invalid token, User not found o Forbidden. Después une el socket a la sala chat:<matchId>, donde se emite new_message con el mismo JSON que devuelve el POST.

La regla de las conexiones

POST /events/:eventId/attend guarda la asistencia y recorre al resto de asistentes. Cada pareja se guarda con el id menor primero, y la restricción única (event_id, user_id_1, user_id_2) hace que repetir la llamada no duplique nada:

for (const attendee of otherAttendees) {
  const u1 = Math.min(user.id, attendee.userId);
  const u2 = Math.max(user.id, attendee.userId);
  await db
    .insert(matchesTable)
    .values({ eventId, userId1: u1, userId2: u2 })
    .onConflictDoNothing();
}

Errores

Todas las rutas responden errores como { "error": "<mensaje>" }. Un error sin capturar lo recoge el último middleware de app.ts con un 500.

El contrato tipa el cliente, pero el servidor solo pasa por Zod la respuesta de /healthz: los cuerpos de POST y PUT se leen sin validar.

06-auth.md

Autenticación

Las cuentas son de Neon Auth (Better Auth gestionado por Neon). La app no guarda contraseñas ni sesiones propias: su tabla users enlaza cada cuenta por auth_id y guarda solo el perfil musical.

Del login al usuario de la base de datos

  1. Login. /auth/sign-in y /auth/sign-up pintan AuthView de @neondatabase/auth-ui dentro de NeonAuthUIProvider: email y contraseña o Google, nombre obligatorio al registrarse y recuperación de contraseña. La sesión queda en una cookie de Neon Auth (credentials: "include").
  2. JWT. Antes de cada petición a la API, getAccessToken() (src/lib/auth.ts) busca un JWT en este orden y se queda con el primero válido:
    1. la cabecera set-auth-jwt de la respuesta de getSession();
    2. el token de la sesión, si ya es un JWT;
    3. authClient.token();
    4. GET <Neon Auth>/token.
  3. Verificación. La API lee Authorization: Bearer y verifica el token con jose contra <NEON_AUTH_BASE_URL>/.well-known/jwks.json, exigiendo que el emisor sea el origen de esa URL. El JWKS se descarga una vez y se reutiliza.
  4. Alta en el primer uso. provisionUser() busca la fila con auth_id = sub. Si no existe, la crea con el nombre y el avatar del token.

El nombre de una cuenta nueva

El token no siempre trae el nombre. profileFromPayload() prueba en este orden: el claim name, la parte del email antes de la @ y, si no hay nada, "Usuario". Cuando el resultado es "Usuario", provisionUser() consulta la tabla neon_auth."user", que Neon Auth mantiene en la misma base de datos, y toma de ahí nombre e imagen. Si una petición posterior trae un nombre mejor o un avatar que faltaba, actualiza la fila.

Invitados

GET /events y GET /events/:eventId usan optionalAuth: con un token válido identifican a la persona y calculan isAttending; sin él, o con uno inválido, siguen como invitado. El resto de rutas usan requireAuth.

En el front, las pantallas de invitado no muestran Conexiones ni Perfil, y los botones de apuntarse llevan a iniciar sesión.

El chat

El handshake de Socket.io lleva el mismo JWT y el id de la conexión. El servidor lo verifica igual que una petición HTTP y además comprueba que la persona es una de las dos de esa conexión antes de unirla a la sala.

Al cerrar sesión o cambiar de cuenta

authClient.signOut() cierra la sesión y la app vuelve a la landing. Si en la misma pestaña entra otra cuenta, AuthQueryClientCacheInvalidator vacía la caché de TanStack Query para que no queden datos de la anterior.

Configuración

VariableDóndePara
NEON_AUTH_BASE_URL (o NEON_AUTH_URL)APIJWKS y emisor esperado. Sin ella, las rutas protegidas responden 401
VITE_NEON_AUTH_URLFrontEl cliente de Neon Auth. Si falta, usa la URL del proyecto de producción escrita en el código

Los logs de pino ocultan Authorization y las cookies de cada petición.

07-data-model.md

Modelo de datos

Neon Postgres con Drizzle ORM. El esquema está en lib/db/src/schema/, una tabla por fichero. Todas usan un serial como clave y un created_at con la hora de inserción.

users

ColumnaTipoNotas
auth_idtextÚnico. El sub de la cuenta de Neon Auth
nametextObligatorio
biotext
avatar_urltextViene de Neon Auth al crear la fila
music_genrestext[]Obligatorio, vacío por defecto
citytext

events

ColumnaTipoNotas
titletextObligatorio
artist_nametextObligatorio
venue_nametextObligatorio
venue_addresstext
datetimestampObligatorio
lat, lngdouble precisionObligatorios; los usa el mapa
genretext
image_urltext
created_by_user_idinteger→ users.id, borrado en cascada

event_attendees

Quién va a qué bolo: event_id → events.id y user_id → users.id, los dos con borrado en cascada. Único por (event_id, user_id), así que apuntarse dos veces no duplica nada.

matches

Una conexión entre dos personas por un bolo: event_id, user_id_1 y user_id_2, con user_id_1 siempre menor que user_id_2. Único por (event_id, user_id_1, user_id_2). Las tres claves ajenas borran en cascada.

messages

match_id → matches.id, sender_id → users.id y content. Borrar una conexión borra su chat.

Cascadas

users ─┬─► events ─┬─► event_attendees
       │           └─► matches ──► messages
       ├─► event_attendees
       ├─► matches
       └─► messages

Borrar a una persona borra los bolos que creó y, con ellos, las asistencias, conexiones y mensajes de todos los que iban. Antes de borrar una cuenta que ha creado bolos hay que pasarlos a otra persona.

Fuera del esquema

  • neon_auth."user". La tabla de cuentas de Neon Auth, en la misma base de datos. La API solo la lee, para completar el nombre de una cuenta nueva (ver Autenticación).
  • Bolos iniciales. Los seis bolos de producción los creó una cuenta de sistema (auth_id = 'system-conciergo') directamente en la base de datos; no hay script de semilla en el repo. Las notas de .agents/memory piden no crear personas falsas: llenarían las conexiones de gente con la que nadie puede hablar.

Cómo se aplica el esquema

No hay migraciones versionadas. pnpm --filter @workspace/db run push ejecuta drizzle-kit push, que compara el esquema de lib/db con la base de DATABASE_URL y aplica la diferencia. Existe también push-force, que no pide confirmación.

Lo que devuelve la API

Las rutas no devuelven filas tal cual. Las convierten con userToJson, eventToJson, messageToJson y enrichMatch: fechas en ISO 8601, null explícito en los opcionales y campos calculados como attendeesCount, isAttending, otherUser, lastMessage o unreadCount.

08-deploy-vercel.md

Deploy en Vercel

Producción: conciergo-sigma.vercel.app. Un solo proyecto de Vercel sirve la web y la API desde el mismo dominio, así que no hace falta CORS entre ellas.

Qué hace Vercel con main

Todo está en vercel.json:

PasoConfiguración
CuándoSolo los pushes a main despliegan; las previews de otras ramas están desactivadas (git.deploymentEnabled)
Instalarpnpm install --frozen-lockfile
Compilarpnpm --filter @workspace/conciergo run build && pnpm --filter @workspace/api-server run build
Estáticosartifacts/conciergo/dist/public
APIapi/index.js como función, con artifacts/api-server/dist/** incluido y hasta 30 segundos por petición
Rutas/api/:path* → /api/index; cualquier otra ruta → /index.html, para que el router del cliente resuelva

api/index.js importa la app la primera vez y la guarda para las siguientes peticiones de la misma instancia. Si la importación falla (por ejemplo, por un módulo que falta en el bundle), responde 500 con {"error":"API failed to start"} y el motivo, en lugar de un error genérico de Vercel.

El build de Vercel no ejecuta el typecheck, así que un error de tipos no bloquea el deploy.

Variables de entorno

VariableLadoPara
DATABASE_URLAPILa cadena de conexión de Neon, con pooling
NEON_AUTH_BASE_URLAPIEl JWKS y el emisor de los tokens (NEON_AUTH_URL también vale)
VITE_NEON_AUTH_URLFrontEl cliente de Neon Auth. Tiene un valor por defecto en src/lib/auth.ts
VITE_GOOGLE_MAPS_API_KEYFrontLa clave de Maps. Es pública por naturaleza: conviene restringirla por dominio en Google Cloud
LOG_LEVELAPIOpcional, info por defecto

Las VITE_* se escriben en el JavaScript durante el build: cambiarlas exige un deploy nuevo.

La base de datos y Neon Auth son el proyecto Neon concert-connect-eu, en Frankfurt.

Lo que no hay en Vercel

  • Socket.io. Una función no mantiene conexiones abiertas, así que el chat no llega en vivo en producción; se ve al refrescar (ver Arquitectura).
  • Migraciones en el deploy. El esquema se aplica a mano con drizzle-kit push antes de desplegar código que lo necesite.

En local

.env.example lista las variables, pero nada carga un .env de la raíz: la API lee process.env tal cual, y Vite solo lee los .env de artifacts/conciergo. Lo más simple es exportarlas en cada terminal:

pnpm install
# terminal 1: DATABASE_URL y NEON_AUTH_BASE_URL exportadas
pnpm --filter @workspace/api-server run dev   # API en :8080, con Socket.io
# terminal 2: VITE_NEON_AUTH_URL y VITE_GOOGLE_MAPS_API_KEY exportadas
pnpm --filter @workspace/conciergo run dev    # Vite en :5173

Vite redirige /api a API_PROXY_TARGET (http://127.0.0.1:8080 por defecto). PORT y BASE_PATH cambian el puerto y la ruta base de la web. En local el chat sí es en vivo.

09-decisions.md

Decisiones

Apuntarse es el match

No hay un segundo feed para valorar personas. Si dos personas van al mismo bolo, quedan conectadas. Es menos incómodo que una app de citas y el chat empieza con un tema de conversación: el concierto.

Borrarse quita el chat

Al dejar de ir, se borran tus conexiones de ese bolo y sus mensajes. La regla es que solo chatea quien va; mantener conexiones de alguien que ya no asiste dejaría chats con gente que no estará allí. Como las rutas del chat comprueban que la conexión existe, no hace falta revisar la asistencia en cada mensaje.

El cartel es público

Eventos, mapa y fichas se ven sin cuenta (optionalAuth). La landing no es un muro: quien llega ve primero qué conciertos hay, y la cuenta se pide al apuntarse.

El contrato manda

openapi.yaml es la fuente de los tipos: Orval genera los hooks del front y los esquemas Zod, y las páginas solo llaman a la API a través de esos hooks. Cambiar una respuesta obliga a tocar el contrato, regenerar y compilar. El servidor aún no valida con Zod lo que recibe; hoy el contrato protege sobre todo al cliente.

Neon Auth en lugar de Clerk

La primera versión, en Replit, usaba Clerk. Al pasar a Vercel en agosto de 2026, el login pasó a Neon Auth: las cuentas viven en la misma base de datos que el producto y la API solo necesita el JWKS para verificar tokens. La fila de users se crea en el primer uso, sin webhook.

La app Express separada del servidor

app.ts exporta la app y index.ts la escucha con Socket.io. Así una función de Vercel puede importar la app sin abrir un puerto. El coste está asumido: en producción no hay chat en vivo.

Base de datos perezosa

El pool de Postgres se crea en la primera consulta. Una instancia nueva de la función que solo atiende /api/healthz arranca sin tocar la base de datos.

Instalaciones con un día de margen

minimumReleaseAge: 1440 hace que pnpm rechace versiones publicadas hace menos de 24 horas. La mayoría de paquetes maliciosos se retiran antes de ese plazo.

Solo main despliega

Las previews de Vercel están desactivadas. Solo hay una base de datos y un proyecto de Neon Auth, así que una preview trabajaría sobre los datos de producción.

10-challenges-results.md

Retos y resultados

Retos

Salir de Replit

El proyecto nació en Replit con Clerk. El 16 y 17 de agosto de 2026 pasó a Vercel y Neon Auth: la API se separó en una app importable para la función, el login se rehízo con @neondatabase/auth-ui y hubo que arreglar el envío del JWT para que apuntarse, las conexiones y el perfil funcionaran con la nueva sesión. Después se limitó el deploy a main para que las previews dejaran de publicarse.

Sacar un JWT de Neon Auth

La sesión de Neon Auth vive en una cookie, pero la API necesita un JWT en la cabecera. El SDK puede entregar ese token por varias vías, así que getAccessToken() prueba cuatro fuentes por orden (ver Autenticación) y la caché reintenta los 401 mientras el token llega.

Cuentas sin nombre

El JWT de una cuenta recién creada puede no traer el nombre, y las primeras conexiones salían como «Usuario». La API lee entonces la tabla neon_auth."user" y corrige el nombre en cuanto lo tiene.

Google Maps

La versión 2 de @googlemaps/js-api-loader quitó la clase Loader; ahora se usan setOptions({ key }) e importLibrary(). Los marcadores avanzados exigen un mapId (la app usa DEMO_MAP_ID), y sin las APIs activadas en Google Cloud el mapa enseña el aviso de Google en lugar de los bolos.

Conexiones que sobrevivían

Al principio, borrarse de un bolo dejaba vivas las conexiones y se podía seguir chateando. Ahora se borran con la asistencia y la cascada se lleva los mensajes.

Lo que falta

HuecoEfecto
Sin Socket.io en VercelEn producción los mensajes llegan al refrescar, no al momento
Contador de mensajesunreadCount cuenta todos los mensajes recibidos; no hay estado de leído, así que nunca baja
Bolos sin ubicaciónEl formulario envía lat: 0, lng: 0: un bolo creado desde la app no sale en el mapa de España
Sin validación en el servidorLos cuerpos de POST y PUT no pasan por los esquemas Zod
Tipos rotos en la webpnpm run typecheck da 8 errores en artifacts/conciergo (ver integración)
Sin tests automatizadosVer Testing · visión
Endpoints sin interfazDELETE /events/:id y GET /events/stats existen pero ninguna pantalla los usa

Resultados

  • La app está en producción con seis bolos en Madrid y Barcelona, login con email o Google y el ciclo completo: ver el cartel, apuntarse, conectar y chatear.
  • El workspace se lee en orden: contrato → código generado → API Express → páginas React. Un cambio en una respuesta se ve en el contrato antes que en el código.
  • Las páginas se cargan por separado y las dependencias grandes van en trozos propios, así que la landing no descarga el mapa ni el chat.

11-angular-skill.md

Front · React

En los otros proyectos este documento repasa cómo se usa Angular. Conciergo es una app React 19 con Vite, así que aquí se cuentan las prácticas de su cliente (artifacts/conciergo).

Rutas

Wouter, con la ruta base de Vite (BASE_PATH) aplicada al router. Cada página es un lazy(); mientras carga, RouteFallback pinta el esqueleto del layout para que la navegación no parpadee. Una ruta desconocida cae en NotFound.

Datos

  • Solo hooks generados. Las páginas no llaman a fetch: usan los hooks de @workspace/api-client-react (useListEvents, useAttendEvent, useSendMessage…), tipados desde el contrato.
  • Un QueryClient con reglas propias: 15 segundos de frescura, refresco al volver a la ventana y al reconectar, tres reintentos para un 401 y ninguno para el resto de 4xx.
  • Invalidación tras escribir. Apuntarse o borrarse invalida eventos, fichas y conexiones de una vez con invalidateAfterAttendance().
  • Conexiones vivas. useMatchesQuery() repite la petición cada 20 segundos con la sesión abierta y conserva los datos anteriores mientras llega la respuesta, para que el badge no parpadee.

Formularios

react-hook-form con zodResolver: el formulario de nuevo bolo valida en el cliente (campos obligatorios, URL de imagen válida) y enseña los errores junto a cada campo.

Interfaz

  • Componentes shadcn/ui sobre Radix en src/components/ui, con Tailwind 4 y una estética de cartel: bordes gruesos, sombras duras y tipografía serif en mayúsculas.
  • Framer Motion para la entrada de tarjetas, mensajes y bloques que aparecen al apuntarse.
  • AppLayout cambia entre barra lateral (escritorio) y barra inferior (móvil), y oculta Conexiones y Perfil a los invitados.
  • La lista de eventos espera a que las imágenes de las tarjetas estén descargadas antes de quitar el esqueleto.

Carga

vite.config.ts separa React, TanStack Query y Framer Motion en trozos propios. En el build del 20 de octubre de 2026 pesan 186 kB, 36 kB y 127 kB sin comprimir (59 kB, 11 kB y 42 kB con gzip). El mapa solo carga la API de Google en /events/map.

En esta página

Visión general Enlaces Stack En cifras Historia Cómo leer esta documentación Producto y features Problema Solución Quién la usa Pantallas Landing (/) Acceso (/auth/sign-in, /auth/sign-up) Eventos (/events) Mapa (/events/map) Nuevo bolo (/events/new) Ficha de un bolo (/events/:id) Conexiones (/matches) Chat (/matches/:matchId) Perfil (/profile) y perfil de otra persona (/profile/:userId) Reglas Arquitectura Vista general Cómo viaja una petición Dos formas de arrancar la API Cómo llega un mensaje Estado en el cliente Base de datos Carga del front Estructura del workspace Comandos Ajustes del workspace Superficie API Salud Usuarios Eventos Conexiones y mensajes Chat en vivo (Socket.io) La regla de las conexiones Errores Autenticación Neon Auth Del login al usuario de la base de datos El nombre de una cuenta nueva Invitados El chat Al cerrar sesión o cambiar de cuenta Configuración Modelo de datos users events event_attendees matches messages Cascadas Fuera del esquema Cómo se aplica el esquema Lo que devuelve la API Deploy Vercel Qué hace Vercel con main Variables de entorno Lo que no hay en Vercel En local Decisiones de producto y tech Apuntarse es el match Borrarse quita el chat El cartel es público El contrato manda Neon Auth en lugar de Clerk La app Express separada del servidor Base de datos perezosa Instalaciones con un día de margen Solo main despliega Retos y resultados Retos Salir de Replit Sacar un JWT de Neon Auth Cuentas sin nombre Google Maps Conexiones que sobrevivían Lo que falta Resultados Front · React Rutas Datos Formularios Interfaz Carga

0% leído

folder_zip Descargar todos (ZIP)
Pregunta sobre Cristian Asistente con IA, solo datos públicos

Es un asistente automático y puede equivocarse. Para algo importante, escribe a Cristian.