Dask
Gestión de academias de baile con un sitio público por academia
La plataforma donde una academia de baile gestiona matrícula, cobros y comunicación con sus estudiantes, y obtiene su propio sitio público en su propio subdominio. Multi-tenant sobre Supabase, con los cobros automatizados con pg_cron y las reglas de acceso verificadas con más de 200 chequeos en SQL.
Pantallas
Stack tecnológico
Visión general
Las academias de baile son negocios hechos de personas: grupos que cambian, estudiantes que van y vienen, mensualidades que hay que perseguir. Casi todas operan entre hojas de cálculo y chats, con comprobantes de pago perdidos en WhatsApp, y el costo es silencioso: horas administrativas, pagos que se escapan y conversaciones incómodas de cobro que desgastan la relación con el estudiante. Bailo salsa y bachata, así que conocía este mundo desde adentro. Dask empezó por el dinero, que es donde más duele, y después creció para darle a cada academia su propio sitio público, con sus colores, en su propio subdominio.
⚡ El desafío
El dinero en una academia pequeña es desordenado: efectivo, transferencias y comprobantes de SINPE enviados como fotos, pagos parciales, promos de dos por uno. Cada academia es un tenant que nunca debe ver los datos de otra, y una misma persona puede ser profesora en una academia y administradora en otra.
✓ La solución
El cobro separa tres cosas que normalmente se mezclan: un cargo es una deuda, un reporte de pago es un reclamo con una foto y una transacción es dinero real. Los cargos mensuales los genera un job de pg_cron en SQL puro, idempotente por estudiante y fecha de vencimiento, con el día de cobro ajustado en los meses cortos. El estudiante sube un comprobante, comprimido en el navegador y guardado en Cloudflare R2 mediante una Edge Function; un administrador lo aprueba, y un trigger de base de datos salda el cargo cuando las transacciones pagadas alcanzan el monto. El orden de los jobs diarios es deliberado: generar a medianoche, marcar vencidos a las 06:00 y enviar recordatorios a las 08:00, hora de Costa Rica.
Impacto y resultados
Cada una en su propio subdominio con su marca y su sitio público
Al automatizar cargos, validación de comprobantes y recordatorios
177 chequeos de acceso y 27 ataques de escalada de privilegios en la suite de gates
Decisiones clave
Cargos, reportes de pago y transacciones son tres tablas
Una deuda, un reclamo y el dinero tienen ciclos de vida distintos. Mantenerlos separados es lo que permite pagos parciales, comprobantes rechazados y pagos por adelantado sin editar la historia.
Id del tenant desnormalizado para un RLS rápido
academy_id se copia en las seis tablas transaccionales y un trigger lo mantiene correcto, así las policies filtran por la propia fila en lugar de hacer join con profiles.
Los helpers de RLS devuelven conjuntos, no escalares
Cuando una persona pudo tener varios perfiles, un helper que hacía LIMIT 1 sobre una columna que ya no era única empezó a devolver una fila arbitraria, en silencio, en 47 de las 76 policies de producción. Primero audité producción en modo solo lectura y después reescribí las 76; borrar los helpers viejos al final hacía fallar la migración si alguna policy todavía los usaba.
Gates de seguridad escritos como ataques
No existía una suite de tests, así que escribí gates en SQL que corren dentro de una transacción y hacen rollback: chequeos de quién puede ver o tocar qué, invariantes contables que deben contar cero violaciones y 27 ataques de escalada de privilegios. Existen porque, antes de agregar permisos finos para profesores, me pregunté si un estudiante podía simplemente volverse profesor, y encontré dos formas.
Un cron que no está en una migración no existe
Staging y producción habían terminado con conjuntos de jobs programados completamente distintos. Los siete jobs ahora viven en migraciones y llaman a las Edge Functions mediante una única función puente que lee su configuración de la base de datos.
Un solo resolvedor para el subdominio
La app, su manifest de PWA y el sitio público en Astro resuelven la academia desde el hostname con la misma lista de nombres reservados, que la base de datos también hace cumplir, así nadie puede registrar admin.getdask.com. El sitio público resuelve el tenant una sola vez en el middleware, desde una única vista.
Destacados técnicos
- ▸ React 18, TypeScript, Vite, react-router, Tailwind v4 y Radix; Zustand limitado a cuatro stores (auth, academia, errores, actualizaciones optimistas) por una regla escrita; 28 carpetas de features
- ▸ Supabase con 54 tablas bajo row-level security, 223 policies y helpers SECURITY DEFINER que filtran perfiles eliminados o transferidos
- ▸ 14 Edge Functions en Deno: subida de comprobantes y flyers a Cloudflare R2, correo transaccional con Resend, Web Push propio con VAPID y un recordatorio de pago de tres toques
- ▸ 7 jobs de pg_cron definidos en migraciones: cargos mensuales, marcado de vencidos, recordatorios, generación de sesiones, y vencimiento y activación de membresías en un orden garantizado
- ▸ Tests de integración con Vitest contra un Supabase real, incluida una suite de abuso de 42 casos, junto a los gates en SQL
- ▸ Sitios públicos de academias en Astro 5 con un editor CMS dentro de la app y vista previa en vivo; semantic-release con conventional commits
- ▸ Radar de deserción con tres señales: horarios que se vacían, deudores y estudiantes con ausencias consecutivas
Aprendizajes clave
Una auditoría encontró que el registro público aceptaba un rol enviado por el cliente. Nunca hay que confiar en un identificador o un rol que venga del cliente; se resuelve en el servidor.
Un RPC que recibía el id de la academia como parámetro era una fuga entre tenants esperando a ocurrir. Ahora resuelve la academia desde la sesión.
El precache del service worker de la app secuestraba la ruta raíz en los subdominios de los tenants y servía el shell de la app en lugar del sitio público de la academia. Dos productos en un mismo dominio necesitan un contrato explícito sobre quién es dueño de cada URL.
Aplastar el esquema para igualarlo a producción y desplegar las migraciones desde CI fueron la misma decisión: dejar de editar producción a mano.
La documentación que admite dónde el producto está mal (rutas huérfanas, estados inalcanzables, bugs de flujo conocidos) es más útil que la que solo describe el camino feliz.