Documentación — Cómo funciona
Última actualización: 6 de agosto de 2026
1. Resumen del flujo
Un organizador conectado crea una cita, que se envía después a un invitado mediante un enlace público único. El invitado no necesita ninguna cuenta: responde desde una página pública, y la respuesta aparece de inmediato en el panel del organizador.
Organizador (conectado)
└─ crea una cita ──▶ appointments (status = pending, invite_token generado)
├─ «Enviar por email» ──▶ función de servidor sendInviteEmail ──▶ Resend
└─ «Copiar enlace» ──▶ /invite/<token>
Invitado (sin cuenta)
└─ abre /invite/<token> ──▶ RPC get_invite(_token) (lectura pública filtrada)
└─ Aceptar / Rechazar ──▶ RPC respond_to_invite(_token, _accept, _message)
└─ status = accepted | declined, responded_at = now()
Organizador
└─ panel: insignia de estado, fecha de respuesta, mensaje del invitado, estadísticas2. Contenido del email de invitación
El email se compone en el servidor en la función sendInviteEmail y se envía mediante Resend. Remitente: valor de RESEND_FROM (por defecto «Cadence <onboarding@resend.dev>»). Asunto: «Cita: {{title}}».
Estructura del cuerpo, en orden:
- Título «Propuesta de cita».
- Saludo «Hola {{guestName}},».
- Bloque de mensaje personalizado (solo organizadores Business).
- Título de la cita en negrita, luego fecha y horario formateados según el idioma.
- Ubicación y descripción, mostradas solo si se han rellenado.
- Botón de acción verde «Confirmar o rechazar» que apunta al enlace de invitación.
- Recordatorio del enlace en texto plano, para clientes de correo que bloquean botones.
- Bloque «Reseñas de Google» (Business): nombre de la empresa, puntuación, enlace de reseñas, enlace de Maps.
Todos los valores dinámicos se escapan en HTML antes de insertarse: ningún contenido introducido por el organizador puede inyectar código en el email.
Variables disponibles
| Variable | Fuente | Uso |
|---|---|---|
| {{guestName}} | contacts.name | Línea de saludo «Hola {{guestName}},». Vacía si no hay ningún contacto asociado. |
| {{title}} | appointments.title | Asunto del email «Cita: {{title}}» y título en negrita en el cuerpo. |
| {{dateRange}} | appointments.starts_at / ends_at | Formateado según el idioma: «martes 12 de agosto, 15:00 → 15:30» (Intl.DateTimeFormat). |
| {{location}} | appointments.location | Línea «Ubicación: …». Todo el bloque se omite si el campo está vacío. |
| {{description}} | appointments.description | Párrafo libre mostrado bajo la fecha. Se omite si está vacío. |
| {{inviteLink}} | origin + /invite/ + appointments.invite_token | Botón «Confirmar o rechazar» + recordatorio del enlace en texto plano al final del email (copiar/pegar). |
| {{inviteMessage}} | profiles.invite_message (Business) | Recuadro verde de mensaje personalizado, mostrado solo para un organizador Business. |
| {{businessName}} / {{googleRating}} / {{googleReviewUrl}} / {{googleMapsUrl}} | profiles.* (Business) | Bloque «Reseñas de Google» al pie del email y en la página de invitación, si la opción está activada. |
Casos en los que el envío no se completa
- no_email : el contacto no tiene dirección de email. El organizador debe copiar el enlace y enviarlo por otro canal.
- no_api_key : la clave de envío no está configurada. El enlace de invitación se devuelve igualmente a la interfaz para copiarlo manualmente.
- provider_error : rechazo del proveedor de email (dirección inválida, dominio no verificado…). El estado de la cita permanece sin cambios.
- Si tiene éxito, invite_sent_at queda con marca de tiempo: este campo alimenta el seguimiento de «enlaces enviados» en las estadísticas.
3. Pantallas de confirmación (página /invite/<token>)
Página pública, sin autenticación, renderizada a partir de la función segura get_invite, que solo devuelve los campos necesarios para la visualización.
- Cargando : esqueleto animado (calendario) mientras no llega la respuesta.
- Enlace inválido o caducado : mensaje de error; no se revela ninguna información sobre el organizador ni sobre la cita.
- Estado «pendiente» : asunto, fecha y horario, duración, ubicación, nombre del organizador, mensaje personalizado (Business), campo libre opcional (500 caracteres máx.), y luego dos botones «Acepto» y «Rechazo».
- Tras la aceptación : insignia verde «Aceptado», confirmación en pantalla, recordatorio de la información práctica y bloque de reseñas de Google si el organizador es Business.
- Tras el rechazo : insignia roja «Rechazado», mensaje indicando que el organizador ha sido notificado; el invitado puede volver a abrir el enlace y cambiar su respuesta mientras la cita no esté cancelada.
- Cita cancelada : los botones de respuesta ya no tienen efecto, el estado mostrado permanece «Cancelado».
El mensaje libre introducido por el invitado se trunca a 500 caracteres en el cliente y a 1000 en la base de datos, y luego se muestra al organizador en la ficha de la cita.
4. Estados de las citas
| Estado | Etiqueta | Desencadenante | Efecto |
|---|---|---|---|
| pending | Pendiente | Al crear la cita (valor por defecto en la base de datos). | El enlace está activo, el invitado puede responder. Se cuenta en «respuestas esperadas». |
| accepted | Aceptado | El invitado hace clic en «Acepto» — llamada a respond_to_invite(_accept = true). | Se rellenan responded_at y response_message; insignia verde en el panel. |
| declined | Rechazado | El invitado hace clic en «Rechazo» — respond_to_invite(_accept = false). | Mismo registro que la aceptación, insignia roja. El horario permanece visible para un recordatorio. |
| cancelled | Cancelado | Acción del organizador desde su panel. | El enlace ya no acepta respuestas (respond_to_invite solo actualiza otros estados). |
5. Flujo del lado del servidor
- Crear / modificar / eliminar : realizado por el cliente autenticado; el aislamiento de datos está garantizado por las reglas de acceso de la base de datos.
- sendInviteEmail: función de servidor protegida. Recarga la cita filtrando por el identificador del solicitante, se niega si la cita no le pertenece, redacta el email, llama a Resend y luego marca el envío con fecha y hora.
- get_invite(_token): función de base de datos con privilegios elevados, la única vía de lectura pública. Solo devuelve los campos de visualización y oculta la información de marca para un organizador que no es Business.
- respond_to_invite(_token, _accept, _message): actualiza el estado, el mensaje y la marca de tiempo de la respuesta. Solo actúa sobre la fila correspondiente al token e ignora las citas canceladas.
- get_plan_usage(): devuelve el plan, el consumo del mes y el indicador de administrador para mostrar las cuotas.
6. Lógica de autorización
- Organizador : acceso completo de lectura y escritura a sus propios contactos y citas, y solo a ellos.
- Invitado no conectado : sin acceso directo a las tablas. Solo puede leer una cita mediante su token y responder a ella. El token es aleatorio (36 caracteres hexadecimales) y no da acceso a nada más.
- Roles : almacenados en una tabla dedicada (usuario, moderador, administrador) y verificados en el servidor; nunca se leen desde el navegador para conceder un permiso.
- Administrador : acceso al área /admin (todas las cuentas, contactos y citas) y exención de las cuotas del plan Free-mium.
- Plan : el cambio de nivel no puede forzarse desde el cliente; solo el proceso de pago de Stripe o un administrador pueden modificarlo.
- Cuotas : aplicadas en la base de datos antes de cada creación — 10/15 (Free-mium), 30/50 (Basic), 50/100 (Standard), 250/500 (Economic) contactos/citas por mes natural, ilimitado en Business y para administradores.
7. Lista de comprobación para el equipo editorial
- Modificar un texto de email: actuar sobre la plantilla HTML de la función de envío, no sobre la página de invitación.
- Modificar una pantalla de confirmación: actuar sobre la página pública de invitación.
- Toda nueva variable debe provenir de un campo ya expuesto por la función de lectura pública.
- Nunca mostrar en la página pública datos de otra cita u otro contacto.
- Probar siempre los tres estados: pendiente, aceptado, rechazado.