MANUAL INTERNO — OBSIDIAN CRM
Documentación de Obsidian
Arquitectura, modelo de datos y guía funcional del CRM multiempresa que corre el negocio de MayaCode y JamStudio: de un mensaje de contacto a una venta cerrada, quién lo atendió y qué tan bien le fue.
00 Qué es Obsidian
Obsidian es el sistema interno donde MayaCode administra su operación comercial: cada mensaje que llega por el sitio web, WhatsApp, Instagram o referido se convierte en un registro que se puede dar seguimiento hasta que se vuelve una venta. No es un producto que se vende — es la herramienta con la que MayaCode se administra a sí misma, y desde esta versión, también administra a JamStudio, su división de diseño, dentro del mismo panel pero con los datos completamente separados.
El flujo comercial que ordena todo el sistema:
Todo lo demás — catálogo, calendario, empleados, reportería — existe para sostener ese flujo o para medirlo.
01 Arquitectura
Dos aplicaciones independientes desplegadas como Cloudflare Workers, compartiendo una sola base de datos D1.
| Framework | vinext | Next.js App Router adaptado a Cloudflare Workers |
| Rutas | app/api/* | Cada endpoint es un route.ts con GET/POST/PATCH/DELETE |
| Base de datos | env.DB | Binding a D1, SQL crudo con marcadores posicionales, sin ORM |
| Migraciones | migrations/ | 25 archivos numerados, aplicados a local y remoto por separado |
| Framework | Vite + React | SPA de una sola página, sin router de URL (estado en memoria) |
| Vistas | src/views/*.tsx | Un componente por sección del menú lateral |
| Cliente API | src/api.ts | Wrapper único api() sobre fetch, adjunta la empresa activa |
| Despliegue | Workers Assets | Build estático de Vite servido como assets del Worker |
_lib/auth.ts verifica el JWT Cf-Access-Jwt-Assertion contra el JWKS de Access (RS256, cacheado en Cache API) y extrae el correo real del usuario en cada request.02 Multiempresa
Una sola base de datos, dos negocios que nunca se mezclan.
Todas las tablas operativas cargan una columna company_id. Un usuario puede pertenecer a una o varias empresas vía user_companies, y el panel se lo pregunta cada vez que hace falta:
resolveTenantContext() corre al inicio de casi cualquier ruta admin: valida el JWT, resuelve o crea el usuario, y confirma que pertenece a la empresa que pide — de lo contrario responde 403. Si el frontend no manda ninguna empresa y el usuario solo tiene una, la usa por defecto; con dos o más, exige que se especifique.
?company_id=2), no como encabezado. Un encabezado personalizado como X-Company-Id se probó primero, pero Cloudflare Access responde el preflight CORS de estas rutas por su cuenta —nunca llega al Worker— y su configuración de encabezados permitidos no incluye ninguno custom. Cualquier encabezado nuevo entre admin.mayacode.dev y api.mayacode.dev debe evitarse por la misma razón; un parámetro de consulta o el cuerpo de la petición no tienen ese problema.Crear una empresa nueva copia automáticamente las etapas de pipeline de una empresa plantilla (por defecto, MayaCode) — sin eso, una empresa recién creada no podría registrar oportunidades.
03 Contactos y clientes
La puerta de entrada: cualquier canal, un solo lugar donde aterriza.
| channel | enum | web · whatsapp · instagram · facebook · phone · referral · other |
| channel_contact_id | text | identificador del canal (ej. número de WhatsApp); único por empresa+canal |
| status | enum | new read archived |
| converted_client_id | fk | se llena al convertir el contacto en cliente |
Convertir un contacto (POST /contacts/:id/convert) crea el client y, si se le agregan productos del catálogo en el mismo paso, además crea de una vez la oportunidad y una cotización en borrador con esas líneas — la ruta más corta entre "alguien escribió" y "ya tiene una propuesta de precio".
04 Pipeline y cotizaciones
Cuatro etapas por defecto, cinco estados de cotización, un cierre que dispara todo lo demás.
Cada empresa tiene sus propias pipeline_stages (nombre, orden, probabilidad por defecto). Las de fábrica: Nuevo · 10% Calificado · 25% Propuesta · 50% Negociación · 75%. Una oportunidad vive en opportunities con status open/won/lost y un assignee_id opcional.
Una cotización rechazada puede reabrirse a borrador. POST /quotes/:id/confirm-sale es el único punto donde una cotización approved se convierte en venta: marca la cotización sold, la oportunidad won, y genera automáticamente una tarea de entrega por cada línea de la cotización — todo en un mismo batch atómico.
05 Catálogo
Lo que cada empresa vende, con su propia tarifa.
products es un catálogo simple de servicios (no hay inventario): nombre, tipo (unico · por_horas · recurrente), unidad y precio, por empresa. Las líneas de una cotización (quote_items) copian el precio del producto al momento de agregarlo, así que un cambio de tarifa después no altera cotizaciones ya emitidas.
06 Empleados y roles
La misma persona, con un puesto distinto en cada empresa.
"Empleado" no es una tabla nueva: es la vista de user_companies para la empresa activa, con un campo libre role (Vendedor, Administrativo, Diseñador…). El mismo usuario puede ser Gerencia en MayaCode y Diseñador en JamStudio — el rol vive en la membresía, no en el usuario.
Es solo clasificación: no hay control de acceso por rol. Cualquier persona autenticada que pertenezca a la empresa ve y edita todo dentro de ella, exactamente igual que antes de que existieran los roles — el modelo de permisos sigue siendo, deliberadamente, plano.
| Empresa | Persona | Rol |
|---|---|---|
| MayaCode | Cristian Castillo | Gerencia |
| Jamileh Picen | Administrativo | |
| Andrea López · Diego Ramírez | Vendedor | |
| JamStudio | Valeria Xitumul | Diseñador |
| Pablo Recinos | Vendedor |
07 Reportería
Quién hizo qué, en qué rango de fechas — exportable a Excel.
Complementa al Dashboard (que muestra el estado actual) con un desglose histórico: por cada empleado de la empresa activa, en un rango from/to elegido a mano, cuenta ventas, cotizaciones por estado, tareas hechas/pendientes y oportunidades ganadas/perdidas/abiertas. La actividad sin responsable asignado se agrupa en una fila "Sin asignar" en vez de perderse.
Ventas y cotizaciones no tienen responsable propio — se atribuyen a través de la oportunidad enlazada (opportunities.assignee_id). El botón Exportar a Excel arma el .xlsx enteramente en el navegador con SheetJS, cargado solo al usarlo para no pesar el resto del panel.
08 Tareas y calendario
Lo que hay que hacer, y cuándo.
tasks (pendiente/en progreso/hecha, prioridad, responsable, fecha límite) y events (reunión/entrega/recordatorio/otro, con hora de inicio y fin) comparten el mismo patrón de asignación por assignee_id que oportunidades. Ambos pueden opcionalmente enlazarse a un cliente u oportunidad, para que el contexto comercial no se pierda al pasar al día a día operativo.
09 Conexión JamStudio
Un sitio distinto, el mismo Obsidian detrás.
jamstudio.mayacode.dev es un sitio estático independiente (HTML/CSS/JS plano, sin build) para la división de diseño de MayaCode. Su formulario de contacto envía a POST /api/contact con {"site": "jamstudio"}; el backend traduce ese campo a company_id: 2, así que cada lead de JamStudio aparece en Obsidian ya clasificado — nunca mezclado con los contactos de MayaCode.
// app/api/contact/route.ts
const SITE_COMPANY_ID = { mayacode: 1, jamstudio: 2 };
const companyId = SITE_COMPANY_ID[body.site] ?? 1;
10 Referencia de API
Todas bajo /api/admin/*, protegidas por Cloudflare Access. Convención constante: {success, data} o {success: false, error}.
11 Despliegue
Wrangler, D1, y siempre backend + frontend juntos.
# aplicar una migración nueva (local, luego producción)
wrangler d1 execute mayacode-db --local --file=migrations/00XX_x.sql
wrangler d1 execute mayacode-db --remote --file=migrations/00XX_x.sql
# desplegar cada proyecto
cd mayacode-backend && npm run deploy # vinext build + wrangler deploy
cd mayacode-admin && npm run deploy # vite build + wrangler deploy
Los dos Workers se despliegan por separado, pero cuando un cambio toca el contrato entre ambos —como agregar un parámetro que el panel debe enviar— se despliegan en el mismo momento: un backend nuevo esperando algo que el frontend viejo todavía no manda deja al panel sin funcionar hasta el segundo despliegue.
Obsidian · MayaCode — documento vivo, generado a partir del estado real del código.