Lalabase
티켓·문서·근무시간·고객대화를 한 곳에 모아 KI로 검색, 소프트웨어 기업 고객 프로젝트용, 오픈소스로 자체 서버·독일 클라우드에 배포
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Reservas AI — SaaS multi-tenant de agentes de reservas Plataforma SaaS multi-tenant para negocios que trabajan con reservas o turnos (restaurantes, barberías, peluquerías/salones de belleza, clínicas, spas, gimnasios, estudios, consultorios, etc.).README 전문 보기 ↗
Plataforma SaaS multi-tenant para negocios que trabajan con reservas o turnos (restaurantes, barberías, peluquerías/salones de belleza, spas, consultorios odontológicos y médicos, fisioterapia, veterinarias, talleres mecánicos, lavaderos de vehículos, academias, gimnasios, estudios, etc.). Cada negocio configura su propio agente de IA, que atiende a sus clientes por Telegram (canal recomendado, gratuito) o WhatsApp, consulta disponibilidad real contra la agenda del negocio y crea, cancela o reprograma reservas — sincronizando automáticamente con Google Calendar, tanto el del negocio como una invitación al propio calendario del cliente.
Es una PWA (Progressive Web App): se puede instalar como app en el celular (Android/iOS) o en escritorio, y toda la interfaz es responsiva.
Wizard de 10 pasos: nombre y tipo de negocio, zona horaria, datos de contacto, horarios de atención, catálogo de servicios (con precio, moneda y duración), recursos (mesas/personal/consultorios), configuración del agente de IA, y conexión de canales — todo antes de llegar al dashboard.
Registro autónomo (sin pasar por soporte), al crear el negocio (create_organization_with_owner):
organizations.trial_ends_at = subscription_expires_at). Un aviso arriba del
panel muestra los días que quedan y lleva a reportar el pago; al vencer, el negocio se suspende solo y la pantalla
de bloqueo deja reportar el pago ahí mismo. Pagar extiende desde el fin de la prueba. Una prueba por persona.signup_disabled_modules). Planes, caja, opiniones y recuperar
clientes los sigue encendiendo soporte.consultar_disponibilidad,
consultar_servicios, obtener_info_negocio).service_id/resource_id reales (nunca los inventa) cuando el negocio tiene catálogo configurado./dashboard/integrations, con credenciales aisladas por tenant.admin o staff, cambiar roles, revocar acceso. La organización siempre
conserva al menos un owner.Estilo minimalista: superficies neutras y un solo color de acento por rubro, más su tipografía de títulos, ícono,
encabezado de inicio y nombres de secciones ("Mesas" y "Comensales" en un restaurante, "Pacientes" en un consultorio,
"Bahías de lavado" en un lavadero). Cada rubro del onboarding tiene su tema, en claro y oscuro, con contraste de texto
verificado por tests (app/src/lib/businessThemes.ts). "Otro" usa la apariencia base.
Desde /soporte/negocios/:id, soporte activa o desactiva los módulos opcionales de cada negocio: Inbox, Clientes,
Servicios, Recursos, Agente IA, Integraciones, Equipo, Fichas, Etapas de atención, y Atención en sitio, Reportes,
Página pública de reservas, Planes, Caja, Opiniones y Recuperar clientes (estos siete arrancan apagados). Inicio, Reservas y Configuración son la base y no se pueden apagar. Un módulo apagado desaparece del menú del negocio y su ruta muestra "Módulo no disponible". Se guarda en
organizations.disabled_modules; un trigger impide que el propio negocio lo cambie. Apagar "Agente IA" solo oculta
la página de configuración: para pausar el agente está el switch de Agente en el mismo panel.
Fila de clientes que llegan sin cita (lavaderos, talleres, barberías): se registra la llegada (nombre, teléfono
opcional, servicio y notas como la placa), se pasa a atención eligiendo el recurso, y se finaliza. Al pasar a
atención, serve_walk_in crea una reserva real con book_reservation (origen walk_in), así respeta la
disponibilidad del recurso y queda en el historial, los reportes y la ficha del cliente. Muestra espera promedio y
se refresca sola para que varias personas del equipo atiendan la misma fila.
Reservas del día y dictado por voz. La misma pantalla lista las reservas de hoy (de cualquier canal) con "Llegó" y
"No vino": check_in_reservation pone a quien llegó en la fila sin crear otra reserva, y al atenderlo/finalizarlo se
usa su reserva original. En restaurantes se registra el número de personas. El botón "Dictar" usa el reconocimiento
de voz del navegador y la Edge Function voice-intake (Gemini) para llenar nombre, teléfono, servicio, personas y
notas; si entendió el nombre, registra la llegada sola a los 3 segundos (se puede cancelar o corregir). Requiere el
secreto GEMINI_API_KEY en InsForge (opcional GEMINI_MODEL, por defecto gemini-2.5-flash); sin él, el botón avisa
que el dictado no está activado.
Tablero de tarjetas y modo pantalla. La fila se ve como un tablero: indicadores grandes (en atención, en espera,
listos, espera promedio), reloj en vivo y tres secciones de tarjetas. "Listos para entregar" reúne lo marcado con la
campana (el cliente recibe el aviso por Telegram o WhatsApp). "En atención" muestra cada atención con la placa (o la
ficha de la reserva) o el ticket en grande, el recurso, el servicio o los productos del pedido, y una barra de tiempo
contra la duración del servicio que se pone roja al pasarse. "En espera" lleva el orden de llegada. "Modo pantalla"
abre el tablero a pantalla completa, sin formularios pero con las acciones de cada tarjeta (atender, campana de
"listo", finalizar, editar), para una pantalla del local desde la que también se opera, y pide que la pantalla no se
apague. Lo atendido hoy queda listado abajo, también en modo pantalla. Con muchas atenciones activas las tarjetas se
achican solas (boardDensity: hasta 6 cómodas, hasta 12 compactas, más de 12 densas) para que quepan más por fila.
Número de ticket. Cada atención (llegada sin cita, reserva que llegó o pedido por QR) recibe un ticket consecutivo
por negocio que vuelve a #001 cada día en la zona horaria del negocio. Lo asigna el trigger assign_walk_in_ticket al
insertar, con un lock por negocio y día; nadie lo puede elegir ni cambiar. Se ve en las tarjetas, en la confirmación
del pedido por QR y en los mensajes de vinculación y de "listo".
Atenciones completadas, ingresos, ticket promedio, cancelaciones y no-show por período (7/30/90 días), reservas por día, y desglose por servicio, recurso y canal, con exportación a CSV.
Cada negocio tiene una página en /r/<slug> para que sus clientes reserven sin chat ni cuenta: eligen servicio, día
y hora libre, y dejan nombre y celular. El inicio del dashboard muestra el enlace, un botón para copiarlo y el código QR
para descargar e imprimir. La API vive en el compute service (/api/public/..., server/src/routes/public.ts): solo
responde por negocios activos con el módulo encendido (mismo 404 en cualquier otro caso), vuelve a verificar la
disponibilidad al reservar, limita 5 reservas cada 10 minutos por IP y tiene un campo trampa contra bots. Requiere
VITE_API_URL en el frontend (URL del compute service) y que APP_URL del server sea la URL del frontend (CORS).
Menú del día (restaurantes). En Carta y servicios cada producto marca en qué días de la semana se ofrece
(services.available_days, 0 = domingo; vacío = todos los días) y hay un filtro "Menú de hoy". La página de pedidos
por QR muestra y acepta solo los productos de hoy en la zona horaria del negocio (services/publicBooking/menuDays.ts),
y el registro de pedidos en Atención en sitio ofrece solo el menú de hoy. No cambia la disponibilidad de reservas de
otros rubros.
Confirmación y recordatorio por chat (todos los rubros). Si el negocio tiene Telegram o WhatsApp conectado, la
confirmación de la reserva ofrece "Avisame por Telegram / WhatsApp". La reserva recibe un notify_code; el botón abre
el chat del negocio con /start <código> (Telegram) o Reserva #<código> (WhatsApp), y ese mensaje vincula el chat
con la reserva (reservations.notify_channel / notify_identity, server/src/services/chatLink/). El cliente recibe
al instante la confirmación con fecha y hora, y el recordatorio de antes de la cita sale por ese chat en vez de por el
teléfono del formulario. Es el mismo mecanismo de los pedidos por QR. Las pantallas de confirmación (reserva y pedido)
tienen un botón para volver al inicio o a la carta sin recargar, conservando nombre y celular.
Pedidos por QR (restaurantes con Atención en sitio). En ese caso la misma página es una carta: el cliente arma un
pedido con uno o varios productos y la cantidad de cada uno (hasta 20 productos distintos y 20 unidades de cada uno),
ve el total y deja su nombre. POST /api/public/businesses/<slug>/orders recibe items: [{ serviceId, quantity }]
(también acepta el serviceId suelto del formato anterior), valida que todos los productos sean del negocio y estén
activos, y guarda en walk_ins.order_items una foto de cada producto (nombre, cantidad, precio y moneda) para que un
cambio posterior en la carta no altere el pedido. El pedido entra a la fila de Atención en sitio con su detalle, el aviso
de "listo" por Telegram o WhatsApp lista todos los productos, y "Registrar venta" registra una venta por producto con
un solo medio de pago. order_items lo escribe solo el server: el equipo lo ve pero no puede modificarlo.
assets)Desde la ficha del cliente se registran vehículos (taller, lavadero: placa única por negocio, marca, modelo,
kilometraje), mascotas (veterinaria), preferencias (salón, barbería, spa, restaurante) o estudiantes y miembros
(academia, gimnasio), y cada reserva se puede vincular a una ficha. Los rubros de salud quedan afuera a propósito:
sus notas son datos sensibles (Ley 1581). Catálogo de campos: app/src/lib/assetTypes.ts.
workflow)Taller, lavadero, veterinaria y restaurante tienen su propio flujo (ej. recibido → diagnóstico → cotización enviada →
aprobado → en reparación → listo para entregar) además del estado de la reserva. Reservas muestra un tablero por
etapas y, al llegar a "listo", un botón para avisarle al cliente por su chat. Definición: app/src/lib/workflows.ts.
plans, apagado por defecto)Catálogo de bonos de sesiones ("5 lavados"), membresías con vigencia ("lavado ilimitado mensual") y tarjetas de sellos
("10 cortes = 1 gratis"), opcionalmente limitados a ciertos servicios. Se venden desde la ficha del cliente (lo vendido
es una copia: cambiar el catálogo no altera lo ya vendido). Al completar una reserva, un trigger en la base
(apply_customer_plans_on_completion) usa la membresía vigente o descuenta una sesión del bono que vence primero, y
suma un sello a cada tarjeta; una misma reserva nunca descuenta dos veces. Al completar la tarjeta queda "Premio listo"
para canjearlo.
cash, apagado por defecto)Registro de lo que realmente entró, con medio de pago (efectivo, Nequi, tarjeta, transferencia, otro): totales del día por medio para el cierre, cobro rápido de reservas completadas (las cubiertas por un bono o membresía no lo piden) y cobro al vender un plan. Un cobro no se edita (solo owner/admin lo borran), y con Caja activa Reportes muestra también "Cobrado en caja".
check_reservation_not_blocked rechaza
(TIME_BLOCKED) cualquier reserva nueva o movida que pise un bloqueo, venga del canal que venga. Las reservas que ya
existían no se tocan; al crear el bloqueo el panel avisa cuántas hay. Cualquiera del equipo puede bloquear; borrar,
solo owner/admin o quien lo creó.surveys, apagado por defecto)Encuesta de 1 a 5 estrellas en /o/<token> (sin cuenta; token de 64 caracteres, vence a los 60 días). Se pide desde
cada reserva completada ("Pedir opinión": WhatsApp, chat o copiar enlace) y, con el compute service corriendo, se
manda sola 2 a 48 horas después de la atención a clientes de Telegram, o de WhatsApp que escribieron en las últimas 24
horas (fuera de esa ventana Meta rechaza texto libre; esas se piden a mano). Quien califica 4 o 5 recibe la invitación
a dejar la reseña en Google (business_profiles.review_url). El cliente responde por las RPC get_public_survey /
submit_public_survey (habilitadas para anon, una sola respuesta); el negocio no puede escribir calificaciones. La
página Opiniones muestra promedio, tasa de respuesta, distribución y comentarios.
reactivation, apagado por defecto)Lista (get_reactivation_candidates) de clientes cuya última atención completada fue hace más de
business_profiles.reactivation_days días (45 por defecto), sin reservas futuras y sin contacto reciente, ordenados
por cantidad de visitas. Cada uno se contacta con un mensaje editable por WhatsApp (enlace wa.me desde el celular del
negocio) o por el chat, y queda registrado en reactivation_contacts.
El motivo de consulta es un dato sensible (Ley 1581): solo se guarda con autorización expresa del paciente
(customers.health_data_consent_at / health_data_consent_source), y lo hace cumplir el código, no el modelo:
resolveHealthDataConsent). Sin ella, el prompt le prohíbe pedir o anotar el motivo y crear_reserva descarta las
notas aunque el modelo las mande (notas_no_guardadas).safetyGuardrails.ts). La veterinaria conserva
solo el aviso: la historia de una mascota no es un dato personal sensible.User → Organization → Business Profile → Agent → Channels → Customers → Conversations → Messages → Reservations → Integrations
Backend: InsForge (PostgreSQL + Auth + Data API, gestionado con @insforge/cli y
@insforge/sdk).
organization_id, y el aislamiento se
garantiza con Row Level Security de PostgreSQL.EXCLUDE constraint (btree_gist) sobre
(organization_id, resource_id, tstzrange(start_at, end_at)), más locks de asesoría (pg_advisory_xact_lock)
para el caso de capacidad total sin recursos individuales.business_profiles, services, resources,
business_hour_periods, agents, agent_rules y el estado de la integración de Google Calendar. El motor de
reservas es el mismo para todos los rubros; lo único específico por rubro es una guía opcional para el agente
(server/src/services/agent/businessTypes.ts: qué datos anotar y límites de seguridad, como no dar diagnósticos)
y las plantillas de servicios del onboarding (app/src/lib/businessTypes.ts).El SDK de browser de InsForge (@insforge/sdk, cliente createClient) mantiene el access token del usuario
encapsulado en memoria, sin getter público. Esto significa que el frontend no puede adjuntar
Authorization: Bearer <token> a un servidor propio externo — sólo puede adjuntarlo automáticamente cuando llama a
insforge.functions.invoke() (Edge Functions de InsForge). Esa restricción de la plataforma determinó la
arquitectura:
| Dónde | Qué vive ahí | Por qué |
|---|---|---|
| Postgres (migraciones) | Tablas, RLS, triggers, RPCs (book_reservation, create_organization_with_owner, accept_organization_invite, etc.) | El frontend llama estas RPCs directamente vía insforge.database.rpc() — el SDK adjunta el token automáticamente porque es una llamada al propio backend de InsForge. |
functions/ (Edge Functions, Deno, desplegadas con @insforge/cli functions deploy) | Las 15 de functions/: conectar/desconectar canales (telegram-*, ycloud-*) y Google, agent-preview, conversations-reply, voice-intake, recibos y pagos de suscripción | Lógica privilegiada que el frontend dispara (necesita el token del usuario + una API key admin para tocar integrations.credentials, bloqueada por columna para authenticated). |
server/ (compute service, Node/Express persistente) | Webhooks de WhatsApp (YCloud), callback de Google OAuth, el long-polling de cada bot de Telegram conectado, el loop de tool-calling del agente | YCloud y Google llaman directo (no el frontend vía SDK); Telegram no llama a nadie (long-polling saliente), pero igual necesita un proceso persistente corriendo. El agente necesita poder seguir procesando en background — algo que una función serverless de vida corta no garantiza. |
Importante para el despliegue: por esto mismo, app/ (el frontend) sí se puede desplegar en una plataforma
serverless/estática como Vercel, pero server/ (el compute service) necesita un host que mantenga un proceso Node
siempre encendido (Railway, Render, Fly.io, un VPS) — Vercel no sirve para esa parte porque el poller de Telegram
necesita estar escuchando todo el tiempo, no solo responder a requests puntuales.
app/): React 18 + TypeScript + Vite + Tailwind CSS + TanStack Query + React Hook Form + Zod +
Radix UI + vite-plugin-pwa.server/): Node + Express + TypeScript, OpenAI SDK (tool-calling), googleapis; WhatsApp por la API de YCloud.functions/): Deno, desplegadas a InsForge.migrations/.crm-clients-wpp/
├── app/ # Frontend: React + Vite + TS + Tailwind
│ ├── public/icons/ # Íconos de la PWA (manifest + apple-touch-icon)
│ ├── scripts/
│ │ └── generate-icons.mjs # Genera los PNG de los íconos sin dependencias nativas
│ ├── vite.config.ts # Config de vite-plugin-pwa (manifest, service worker)
│ └── src/
│ ├── pages/
│ │ ├── auth/ # Login, registro, recuperar contraseña
│ │ ├── onboarding/ # Wizard de 10 pasos + steps/
│ │ └── dashboard/ # Inicio, Inbox, Reservas, Clientes, Servicios, Recursos,
│ │ # Agente, Integraciones, Equipo, Configuración
│ ├── components/
│ │ ├── ui/ # Primitivas (button, input, select, dialog, ...)
│ │ ├── layout/ # DashboardLayout (sidebar/drawer responsivo)
│ │ └── AcceptInvites.tsx, RequireAuth.tsx, RequireOrganization.tsx
│ ├── hooks/ # useAuth, useOrganization, usePendingInvites
│ └── lib/ # insforgeClient, functionsClient, queryClient, currency
├── server/ # Compute service: Node + Express + TS
│ ├── src/
│ │ ├── routes/ # webhooks (YCloud, Telegram), integrations (Google callback), public, health
│ │ ├── services/
│ │ │ ├── agent/ # promptBuilder, tools, toolExecutors, agentRuntime, coreRules
│ │ │ ├── availability/ # motor de disponibilidad (horarios, recursos, capacidad)
│ │ │ ├── reservations/ # createReservation/cancel/reschedule (vía RPCs + Google sync)
│ │ │ ├── customers/ # findOrCreateCustomerByPhone, updateCustomer
│ │ │ ├── conversations/ # handleInboundMessage: lógica compartida entre canales
│ │ │ ├── google/ # OAuth callback + Google Calendar (crear/actualizar/borrar eventos)
│ │ │ ├── ycloud/ # WhatsApp Business (coexistencia)
│ │ │ ├── whatsapp/ # envío de WhatsApp (YCloud)
│ │ │ └── telegram/ # long-polling multi-tenant (telegramPollingManager)
│ │ ├── middleware/ # rateLimit, errorHandler
│ │ ├── config/env.ts # Validación de variables de entorno
│ │ └── lib/ # insforge (admin client), openai, logger
│ └── tests/ # vitest: servicios del server y permisos de las Edge Functions (tests/functions)
├── functions/ # Edge Functions (Deno), invocadas por el frontend vía insforge.functions.invoke()
├── migrations/ # Esquema completo + RLS + RPCs, en orden (npx @insforge/cli db migrations up)
├── db-tests/ # Test de aislamiento multi-tenant (SQL plano, correr con psql)
├── scripts/
│ ├── deploy-functions.mjs # Deploya todas las Edge Functions de una sola vez
│ ├── function-bundle.mjs # Empaqueta en un archivo las funciones que importan código del repo
│ └── changed-functions.mjs # Qué funciones publicar según los archivos que cambiaron en un push
└── .env.example
npm install
cp .env.example .env
InsForge es un servicio en la nube: todo se gestiona con npx @insforge/cli.
# 1. Autenticarse (abre el navegador)
npx @insforge/cli login
# 2. Crear un proyecto nuevo (o `link` si ya tenés uno)
npx @insforge/cli create
# 3. Aplicar todas las migraciones del esquema, en orden
npx @insforge/cli db migrations up --all
# 4. Obtener las credenciales para tu .env
npx @insforge/cli secrets get ANON_KEY
Completá en .env:
VITE_INSFORGE_URL / INSFORGE_URL: la URL de tu proyecto InsForge.VITE_INSFORGE_ANON_KEY: la ANON_KEY obtenida arriba.INSFORGE_API_KEY: una API key admin (Project Settings → API Keys en el dashboard de InsForge).En Configuración → Ubicación el negocio carga su dirección, indicaciones para llegar (piso, local, punto de
referencia) y, opcionalmente, el enlace de Google Maps que copia desde Compartir (solo https://). Sin enlace se
usa una búsqueda de Google Maps con la dirección (server/src/utils/location.ts). El agente comparte la dirección y
el enlace cuando el cliente pregunta cómo llegar y al confirmar una reserva; la página de reservas muestra "Cómo
llegar" y el correo de confirmación, "Ver ubicación en el mapa". Las indicaciones y el enlace necesitan la migración
20261029090000_business-location.sql; antes de aplicarla, Configuración solo muestra la dirección.
La pantalla /forgot-password funciona con los dos métodos de recuperación de InsForge (Authentication en el
dashboard): con código, la persona lo escribe en la misma pantalla; con enlace, el correo lleva a
/reset-password, donde elige la contraseña nueva. Para el enlace, agregá https://<tu-dominio>/reset-password a
las URLs de redirección permitidas de InsForge. El correo lo manda InsForge: si no llega, revisá ahí los envíos y el
límite de correos del plan (los correos de reservas usan el mismo servicio). Se puede reenviar cada 60 segundos.
migrations/20260830121700_seed-demo-data.sql crea 3 organizaciones demo completas (restaurante, barbería, salón
de belleza) — horarios, servicios, recursos, en pesos colombianos — sin owner asignado. Para explorarlas: registrate
normalmente (/register) y llamá la RPC claim_demo_organization('<id-de-la-org>') (los 3 IDs están en el propio
archivo de seed) para convertirte en owner de una de ellas.
node scripts/deploy-functions.mjs
o una por una:
npx @insforge/cli functions deploy <slug> --file functions/<slug>.ts
InsForge recibe un solo archivo por función. Las que importan código del repo (agent-preview, que usa las
instrucciones del agente real de server/src/services/agent/systemPrompt.ts, y las que usan functions/_shared/,
como el cifrado de credenciales) se empaquetan antes con esbuild (scripts/function-bundle.mjs) en los dos scripts
de despliegue; no las publiques con el comando de una por una. functions/_shared/ es código compartido, no una
función.
Sin terminal: el workflow de GitHub Actions Deploy Edge Functions publica en cada push a main solo las
funciones afectadas por lo que cambió en todo el push (aunque traiga varios commits): scripts/changed-functions.mjs
mira de qué archivos depende cada función (su propio .ts, functions/_shared/ o código del server que importa).
También se puede correr a mano (Actions → Deploy Edge Functions → Run workflow, con los nombres separados
por espacio o vacío para todas). Usa los secretos del repositorio INSFORGE_URL e INSFORGE_API_KEY (los mismos
valores que el compute service en Railway) y llama a la API de InsForge con scripts/deploy-functions-api.mjs.
Cada función necesita estos secretos en InsForge (npx @insforge/cli secrets add <KEY> <VALUE>): INSFORGE_BASE_URL
y API_KEY (reservados, ya provistos por InsForge), más OPENAI_API_KEY, OPENAI_MODEL, GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, OAUTH_STATE_SECRET e INTEGRATIONS_ENCRYPTION_KEY (estos dos
últimos con el mismo valor que en el .env del compute service).
OPENAI_API_KEY en .env y como secreto de InsForge (lo usan tanto server/ como agent-preview).OPENAI_MODEL controla el modelo usado por el agente desde un único lugar.OPENAI_BASE_URL es opcional: permite apuntar a un endpoint compatible (ej. OpenRouter) para desarrollo sin
costo de OpenAI directo.A diferencia de WhatsApp, Telegram no cobra por mensaje ni requiere aprobación de negocio. El compute service lo
atiende de dos formas (TELEGRAM_MODE):
polling (por defecto): long-polling (telegramPollingManager.ts). No necesita URL pública, pero solo
puede correr una instancia del server (dos instancias leyendo el mismo bot = 409 y mensajes duplicados).webhook (para escalar): Telegram le envía cada mensaje a POST /api/webhooks/telegram/:organizationId
(telegramWebhookManager.ts), con un secreto por negocio derivado de TELEGRAM_WEBHOOK_SECRET. Permite varias
instancias. Requiere TELEGRAM_WEBHOOK_BASE_URL (URL pública HTTPS del server, ej.
https://reservas-server.up.railway.app) y TELEGRAM_WEBHOOK_SECRET (16+ caracteres al azar). Una tarea
programada registra el webhook de cada bot conectado (máx. 1 minuto después de conectarlo). Volver a polling
es seguro: cada poller quita el webhook del bot sin perder mensajes pendientes./newbot, seguí las instrucciones y copiá el token
(formato 123456789:AA...)./dashboard/integrations, pegá ese token en la tarjeta "Telegram" y conectá — esto valida el token contra
la API de Telegram y lo guarda en integrations.credentials.polling, en el próximo
refresh (máx. 30s); en webhook, cuando la tarea de sincronización le registra el webhook (máx. 1 minuto).webhook, los mensajes que sigan llegando se ignoran).Cada negocio conecta su propio número de WhatsApp Business en coexistencia: sigue usando la app en el
celular y el agente atiende por la API oficial de Meta, a través de YCloud. El paso a paso para el negocio está en
la sección 23 del manual de usuario (docs/manual/).
/dashboard/integrations → WhatsApp Business (YCloud) pega la API key. La Edge Function
ycloud-connect valida la clave, toma el número y registra en YCloud el webhook
https://<compute-service>/api/webhooks/ycloud/<organization_id> con su propio secreto.YCloud-Signature de cada evento (HMAC-SHA256, 5 minutos de tolerancia),
responde al cliente con el agente y pausa al agente 30 minutos cuando el negocio escribe desde la app.SERVER_PUBLIC_URL (secreto de las Edge Functions en InsForge)
o, si no está, de VITE_API_URL del frontend.reservasia_recordatorio_reserva (español) y espera a que Meta la apruebe (suele tardar de minutos a
unas horas); mientras tanto el recordatorio se salta y se reintenta cada 10 minutos. El estado queda en
integrations.metadata.reminder_template y en los logs (ycloud_reminder_template_*). Meta cobra cada
plantilla enviada según su tarifa para el país. Por Telegram el recordatorio sale como mensaje normal, gratis.Sólo el negocio pasa por el consentimiento de Google (una vez). El cliente que reserva nunca necesita su propia cuenta de Google: si da su email al agente, se lo agrega como invitado al evento y Google le manda la invitación por correo — si acepta, le queda guardado en su propio calendario.
.../auth/calendar.events y .../auth/userinfo.email, y tu email como usuario de prueba (modo "Testing").http://localhost:3011/api/integrations/google/callback # desarrollo
https://<tu-dominio-del-compute-service>/api/integrations/google/callback # producción
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET y GOOGLE_REDIRECT_URI en .env y como secretos de
InsForge./dashboard/integrations, cada organización conecta su propia cuenta de Google (tokens guardados por
organization_id, nunca en una fila global).La plataforma nació pensada para Colombia: el default de moneda de negocios y servicios nuevos es COP (peso
colombiano). No está hardcodeado — cada negocio puede cambiar su moneda desde Configuración, y cada servicio
tiene su propio selector de moneda al crearlo o editarlo (app/src/lib/currency.ts), así que la plataforma sigue
sirviendo para otros países sin cambios de esquema.
La app es instalable como Progressive Web App:
vite-plugin-pwa, configurado en app/vite.config.ts): nombre, ícono, theme_color de marca,
display: "standalone" (se abre sin barra de navegador).app/scripts/generate-icons.mjs (sin dependencias nativas de imágenes) — 192px, 512px,
512px "maskable" (Android) y apple-touch-icon (iOS). Para regenerarlos tras cambiar el color de marca:
node app/scripts/generate-icons.mjs
app/src/sw.ts, modo injectManifest de vite-plugin-pwa): precachea el shell de la
app (HTML/JS/CSS/íconos) para que abra instantáneo, y además escucha los eventos push/notificationclick (ver
Notificaciones push). Los datos del negocio (reservas, conversaciones) siempre se piden
en vivo a InsForge, nunca se sirven desde caché.app/src/components/InstallAppButton.tsx): en
Android/Chrome dispara la instalación de un clic usando el evento beforeinstallprompt, capturado globalmente en
app/src/hooks/useInstallPrompt.tsx (un React Context montado una única vez en App.tsx, para que el evento —
que el navegador dispara sólo una vez por sesión — no se pierda si llega mientras el usuario está en otra
pantalla). Si el navegador no ofrece ese evento (o en iOS, que no lo tiene), el botón muestra instrucciones
manuales según la plataforma detectada.npm run dev), es el comportamiento esperado. Para
probar la instalación real:
cd app
npm run build
npm run preview
y abrí la URL que imprime desde el navegador del celular (misma red) o desde Chrome/Edge en desktop.Todo el dashboard es responsivo: el menú lateral se convierte en un drawer con botón de hamburguesa en mobile (con altura fija al viewport para que esa barra nunca se desplace al scrollear el contenido), y el Inbox (que en desktop muestra 3 columnas) pasa a navegación por paneles (lista → conversación, con la info del cliente en un diálogo).
Cuando el agente de IA confirma una reserva nueva (por Telegram/WhatsApp), el negocio recibe una notificación push con sonido en el celular — sin necesidad de tener la app abierta.
app/src/sw.ts) escucha el evento push y muestra la notificación; al tocarla, enfoca la
pestaña de la app ya abierta o abre una nueva en /dashboard/reservations.push_subscriptions, aislada por organización con RLS.server/src/services/notifications/pushService.ts) manda el push, en paralelo a todas las
suscripciones de esa organización, cuando se crea una reserva vía el agente
(server/src/services/reservations/reservationsService.ts). Es best-effort — igual que la sincronización con
Google Calendar: si falla el envío, la reserva ya quedó creada de todas formas. Las reservas creadas manualmente
desde el dashboard no disparan push (quien las crea ya está mirando el dashboard).npx web-push generate-vapid-keys
Completá con el par que te devuelva:
.env: VAPID_PUBLIC_KEY y VAPID_PRIVATE_KEY (compute service) + VITE_VAPID_PUBLIC_KEY (frontend, mismo
valor que VAPID_PUBLIC_KEY)..env de server/ en el host donde lo despliegues.Sin estas variables, la funcionalidad queda deshabilitada automáticamente (el botón de Configuración explica que el navegador/dispositivo no la soporta) — el resto de la app sigue funcionando igual.
npm run dev
Levanta en paralelo:
http://localhost:3011 (webhooks de WhatsApp, callback de Google, poller de Telegram)http://localhost:5173Las Edge Functions no tienen modo "dev local": se prueban desplegándolas a InsForge e invocándolas desde la app.
npm test
server (vitest, con InsForge simulado, sin proyecto real): motor de disponibilidad, agente y sus
herramientas, canales (Telegram, YCloud), reservas y avisos, página pública, y en server/tests/functions los
permisos de las 16 Edge Functions (sin sesión, de otro negocio, staff donde solo puede dueño/admin).app (vitest): lógica del frontend y cabeceras de seguridad de vercel.json.VITE_API_URL=https://api.e2e.test VITE_INSFORGE_URL=https://insforge.e2e.test VITE_INSFORGE_ANON_KEY=e2e \
npm run build -w app && npm run e2e -w app
Playwright corre la app compilada en Chromium con el servidor y InsForge simulados (app/e2e/fixtures.ts):
reserva en la página pública (con la casilla de autorización), pedido por QR e inicio de sesión. La primera vez:
npx playwright install chromium dentro de app/.
PGHOST=localhost PGUSER=postgres PGPASSWORD=postgres PGDATABASE=postgres scripts/test-db.sh
Sobre un Postgres vacío (nunca un proyecto InsForge real: crea roles, esquemas y datos), aplica
db-tests/insforge-stub.sql (roles, auth.uid(), auth.users, storage), todas las migraciones en orden, y corre:
db-tests/tenant-isolation.sql: un owner/staff de una organización no puede leer, insertar, actualizar ni
borrar datos de otra; staff no ve integraciones ni puede eliminar la organización; el EXCLUDE constraint
bloquea reservas solapadas.db-tests/modules-billing-walkins.sql: solo soporte cambia módulos, estado y vencimiento de la suscripción;
Atención en sitio y Reportes arrancan apagados; la fila de atención queda aislada por negocio; atender crea
una reserva real, una bahía ocupada se rechaza y finalizar la completa.db-tests/assets-stages.sql: fichas aisladas por negocio, placa única, no se vincula una ficha ni un cliente de
otro negocio, y el cambio de etapa queda registrado..github/workflows/ci.yml corre en cada push a main y en cada pull request: lint, typecheck, tests, build,
npm audit (falla con vulnerabilidades altas, salvo las aceptadas en scripts/audit-check.mjs), la prueba de punta
a punta y los tests de base de datos de arriba contra un Postgres 16. Además corren CodeQL (codeql.yml) y la
búsqueda de claves (secret-scan.yml, gitleaks).
npm run build
app/): build estático — se puede desplegar en Vercel, Netlify, cualquier CDN, o
npx @insforge/cli deployments deploy app. Sólo necesita las variables VITE_*.server/): necesita un host que mantenga un proceso Node siempre encendido — no sirve
Vercel para esta parte (tareas programadas y, en modo polling, el poller de Telegram). Alternativas con nivel
gratuito: Railway, Render, Fly.io; o npx @insforge/cli compute deploy; o un VPS con PM2.acquire_scheduler_lease) y cada vuelta
corre en una sola instancia. Con TELEGRAM_MODE=webhook el server se puede replicar sin duplicar nada.npx @insforge/cli functions deploy <slug> --file functions/<slug>.ts por cada una, o
node scripts/deploy-functions.mjs para todas.npx @insforge/cli db migrations up --all, o sin terminal desde
GitHub: Actions → Deploy database migrations → Run workflow (sin marcar "aplicar" solo muestra las pendientes)..env.example como variables de entorno del proveedor elegido.organizations + organization_members + roles owner/admin/staff.SECURITY DEFINER
(SET search_path = pg_catalog, public, pg_temp) para evitar recursión.integrations.credentials), cifrada además a nivel de aplicación
(AES-256-GCM) antes de guardarse.EXCLUDE constraint + locks de asesoría.book_reservation valida que el servicio/recurso pertenezcan a la organización que reserva (aislamiento
multi-tenant también a nivel de datos referenciados, no sólo de filas propias).GET /api/health del compute service responde 200 mientras el server está vivo. Conviene un
monitor externo gratuito (por ejemplo UptimeRobot, cada 5 minutos) que avise si deja de responder.server/src/lib/alerts.ts): si el mismo tipo de falla pasa 5 veces en 10 minutos, se
manda un correo a ALERT_EMAIL (variable de Railway; varios correos separados por coma), como mucho uno por
hora por tipo. Tipos: el agente no pudo consultar a la IA (agent_ai), mensajes de WhatsApp/Telegram que no se
pudieron procesar (channel_message), una tarea programada que falla (scheduled_job:<nombre>) y errores 500
del server (server_error). El correo nunca lleva datos de clientes, solo el tipo de falla y el mensaje técnico.
Sin ALERT_EMAIL la alerta queda en el log como alert_triggered. Usa la misma cuota de correos de InsForge.server/src/services/agent/agentRateLimit.ts):
hasta 10 turnos del agente por minuto por cliente y 120 por minuto por negocio. Un mensaje frenado igual queda
guardado (se ve en la Bandeja y el agente lo lee en el siguiente turno), y el cliente recibe una sola vez el aviso
de que le responden en un minuto. Vive en memoria del server (alcanza con una sola instancia, como hoy).Con esto se cierran los últimos pendientes técnicos que quedaban documentados en esta sección; lo único que sigue
en esta lista es la decisión deliberada de arriba (no un pendiente real). Resuelto más recientemente: las
credenciales de integraciones (integrations.credentials: tokens de Google, API keys de YCloud, bot tokens de
Telegram) ahora se cifran a nivel de aplicación (AES-256-GCM, clave derivada de INTEGRATIONS_ENCRYPTION_KEY) antes
de guardarse, además del permiso de columna que ya tenían — tanto el compute service (Node) como las Edge Functions
que conectan/desconectan cada canal (Deno) cifran y descifran con la misma clave, así que cualquiera de los dos
lados puede leer lo que escribió el otro; las filas que ya estaban conectadas antes de este cambio se migraron con
server/scripts/backfillEncryptIntegrationCredentials.ts (idempotente: tolera texto plano y cifrado ya existente,
así que es seguro volver a correrlo).
Resuelto desde la última revisión de esta lista: se retiró por completo la integración anterior con Twilio (código,
Edge Functions twilio-connect/twilio-disconnect, variables de entorno, UI y textos legales) — WhatsApp funciona
exclusivamente por YCloud. De paso se corrigieron dos lugares que, desde que existe YCloud, habían quedado
reconociendo solo "twilio" como canal de WhatsApp conectado y por eso nunca mostraban el estado real: el panel de
soporte (lista y detalle de negocio) y los enlaces "Avisame por WhatsApp" de pedidos/reservas (notifyLinks.ts); se eliminó la Edge Function check-availability (quedó huérfana
cuando la página pública de reservas pasó a pedirle disponibilidad al compute service — /businesses/:slug/availability
— y ya había quedado desactualizada: no conocía ni el horario por recurso ni los bloqueos de agenda, los dos
agregados más recientes al motor de disponibilidad real en server/src/services/availability/availabilityService.ts);
política de seguridad de contenido pasada a modo bloqueo
(Content-Security-Policy en app/vercel.json, tras auditar que no hay scripts/estilos/iframes que la violen);
invitaciones de equipo ahora avisan por email (insforge.emails.send, mejor esfuerzo — si el plan no tiene envío
de correo habilitado, la invitación igual se crea); login con Google/GitHub y con código por email (sin
contraseña) agregados en /login y /register; bundle del frontend ya dividido por rutas y por vendor
(React.lazy + manualChunks en vite.config.ts), ningún chunk pasa los 500kB.