Construyendo Remanso: notas de un agendador white-label para terapia
La primera psicóloga para la que construí esto agendaba como casi todas: por WhatsApp, de memoria, y rezando para que nadie pidiera reagendar un domingo. El problema no era que le faltara una función — era que reservar, recordar y entrar a la sesión eran tres cosas separadas que dependían de que un humano las cosiera a mano cada vez. Remanso es lo que construí para unirlas en un solo flujo, y estas son las notas de ingeniería de haberlo hecho.
La decisión que define el proyecto: es un producto, no un sitio
Pude haber hecho la página de una psicóloga. En vez de eso decidí, desde la primera línea, que esto fuera un producto white-label: un agendador que se rebrandea para el siguiente consultorio cambiando configuración, no bifurcando el repo. Esa decisión es la que ordena todo lo demás.
En la práctica significa una regla tonta de tan simple: el nombre del cliente no existe en el código. El nombre del consultorio, los colores, el contacto, los servicios, los horarios — todo vive en configuración y variables de entorno. Si un nombre de cliente se cuela en una plantilla o un correo, no es un detalle: es un bug. Montar el segundo consultorio debe ser llenar config, no abrir un editor de búsqueda y reemplazo.
Hacerlo configurable a posteriori es reescribir. Cocinarlo en la base desde el día uno es solo disciplina.
El paciente nunca se loguea
La tentación obvia era darle al paciente una cuenta. Me negué. Nadie va a crear un usuario y una contraseña para pedir una cita de terapia — y obligarlo es la forma más segura de perder la reserva justo en el momento de mayor fricción.
Así que toda la experiencia del paciente — reservar, cancelar, reagendar, bajar la cita a su calendario — funciona contra un código público por cita. El código es la credencial, con su propio alcance acotado en el servidor: te deja tocar esa cita y nada más. Cero contraseñas que olvidar, cero pantalla de registro entre el paciente y su horario. El backend sigue sin confiar en el cliente; simplemente la cosa que autoriza es un código de un solo propósito en lugar de una sesión.
Reusar el módulo de Wallet sin contaminar nada
El pase de Google Wallet ya lo tenía resuelto de otro de mis productos, y reusarlo era obviamente lo correcto — pero compartir el mismo issuer entre dos productos es exactamente donde las cosas se ensucian. Si las clases de pase de Remanso y las del otro producto viven bajo el mismo issuer sin separación, terminas con el pase de uno apareciendo donde va el del otro.
La solución fue aburrida a propósito: un prefijo guard en los IDs de clase que aísla a Remanso de cualquier otra cosa bajo ese issuer. Reusar infraestructura es barato; reusarla sin aislamiento es cómo te metes un bug de producción que solo ves cuando ya es el pase equivocado en el teléfono de alguien.
WhatsApp manual y SMTP, a propósito
Pude integrar la API de WhatsApp Business y un proveedor de correo con plantillas y webhooks. Para un primer despliegue, no valía la pena. El correo transaccional sale por nodemailer sobre SMTP — confirmaciones, reagendas y recordatorios, con la marca del consultorio — y para los momentos que de verdad piden un humano, un botón de WhatsApp click-to-send abre el chat con el mensaje ya armado.
No es pereza, es enfoque. El trabajo de un MVP es validar el flujo que crea valor — reservar, recordar, llegar a la sesión — no acumular plomería que nadie pidió todavía. La integración pesada se gana su lugar cuando un cliente real la necesita, no antes.
SSR para un negocio local
Remanso es un sitio de negocio local que la gente abre desde el teléfono, casi siempre desde una búsqueda. Eso vuelve al SSR una decisión de producto, no un capricho técnico: la página tiene que pintar rápido y ser indexable, no mandar una pantalla en blanco mientras carga un bundle. El front es Angular 22 + Tailwind 4 con renderizado en servidor, e i18n con Transloco porque el contenido es bilingüe desde el inicio. El primer pintado lo paga el servidor; el SEO lo agradece el negocio.
La trampa silenciosa: la hora de pared en UTC
El bug que más fácil se cuela en un sistema de citas es el desfase de una hora. El paciente ve las 4:00, la cita cae a las 5:00 en el calendario de la psicóloga, y el .ics dice otra cosa — porque cada capa interpretó la fecha a su manera y nadie acordó una convención.
La cita atraviesa demasiados sistemas — la reserva, el calendario de Google, el Meet, el .ics, el pase de Wallet, el recordatorio — como para improvisar la zona horaria en cada uno. Así que la hora de pared se maneja con un solo criterio de UTC de punta a punta, para que la hora que ve el paciente, la que cae en el calendario y la que viaja en el pase sean la misma hora. Es de esas cosas que no se notan cuando están bien y arruinan la confianza en el producto cuando están mal.
Y sí, está probado y desplegado
Nada de esto vive en mi máquina. El backend corre en Railway con Postgres y Redis administrados; el front en Firebase App Hosting con dominio propio; las migraciones de Prisma corren al arrancar el contenedor. Los recordatorios y el correo salen de colas sobre Redis (Bull) con offsets configurables, fuera del camino del request. Trabajé con asistencia de agentes para moverme rápido, pero la barra no cambió: tipos estrictos, pruebas donde importan, y desplegar por pipeline en vez de a mano.
Qué aprendí
“Producto, no sitio” es una restricción de diseño, no una de marketing. En el momento que prohibí el nombre del cliente en el código, todo lo demás se acomodó solo: la config se volvió la fuente de verdad, el rebrand se volvió trivial, y el segundo cliente dejó de ser un fork para volverse un archivo de configuración.
Lo simple a propósito no es deuda técnica. WhatsApp manual y SMTP no son atajos de los que me avergüence — son la respuesta correcta a la pregunta correcta para un MVP. La deuda es construir la integración pesada antes de tener a quién le sirva.
La consistencia horaria es ingeniería invisible. Nadie te felicita porque la cita cae a la hora correcta en cinco sistemas distintos. Pero es justo el tipo de cosa que, cuando falla, hace que un paciente desconfíe de toda la plataforma — así que se diseña, no se improvisa.