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.

Paneladmin.mayacode.dev
APIapi.mayacode.dev
Base de datosCloudflare D1 (SQLite)
AutenticaciónCloudflare Access

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:

Contacto Cliente Oportunidad Cotización Venta Tareas de entrega

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.

mayacode-backendapi.mayacode.dev
FrameworkvinextNext.js App Router adaptado a Cloudflare Workers
Rutasapp/api/*Cada endpoint es un route.ts con GET/POST/PATCH/DELETE
Base de datosenv.DBBinding a D1, SQL crudo con marcadores posicionales, sin ORM
Migracionesmigrations/25 archivos numerados, aplicados a local y remoto por separado
mayacode-adminadmin.mayacode.dev
FrameworkVite + ReactSPA de una sola página, sin router de URL (estado en memoria)
Vistassrc/views/*.tsxUn componente por sección del menú lateral
Cliente APIsrc/api.tsWrapper único api() sobre fetch, adjunta la empresa activa
DespliegueWorkers AssetsBuild estático de Vite servido como assets del Worker
Autenticación: Cloudflare Access protege ambos dominios bajo una sola Access Application. Eso basta para bloquear la entrada, pero el backend necesitaba saber quién hace cada petición — algo que Access por sí solo no expone a la aplicación. _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:

MayaCode — id 1 JamStudio — id 2

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.

Detalle no obvio: la empresa activa viaja como parámetro de consulta (?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.

contactsmensajes entrantes
channelenumweb · whatsapp · instagram · facebook · phone · referral · other
channel_contact_idtextidentificador del canal (ej. número de WhatsApp); único por empresa+canal
statusenumnew read archived
converted_client_idfkse 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.

draft sent approved sold

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.

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.

EmpresaPersonaRol
MayaCodeCristian CastilloGerencia
Jamileh PicenAdministrativo
Andrea López · Diego RamírezVendedor
JamStudioValeria XitumulDiseñador
Pablo RecinosVendedor

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}.

GET/companiesempresas del usuario autenticado
POST/companiescrea empresa + copia etapas de pipeline
PATCH/companies/:idnombre, slug, color, logo
GET/employeesempleados de la empresa activa, con rol
POST/employeescrea/reutiliza usuario y lo asigna con rol
PATCH/users/:id/companiescambia el rol de una membresía
GET/reports?from&todesglose por empleado en un rango de fechas
POST/contacts/:id/convertcontacto → cliente (+ oportunidad + cotización)
POST/quotes/:id/confirm-salecotización aprobada → venta + tareas de entrega
GET/statsKPIs del Dashboard (estado actual, sin rango)

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.