Arquitectura
La arquitectura de EMA Vault refleja su naturaleza de back-office
interno: un solo worker Hono sirve la API y la SPA, dos backends
de Supabase separados (uno principal en emahub y otro de solo
lectura para Ema Well), y un puñado de servicios externos
controlados —Anthropic para AI, GitHub para sync de issues, Stripe
para los productos B2C que viven en Vault, Loops para correo. Lo
que distingue a Vault del resto del ecosistema no es la complejidad
sino la anchura del dominio: el mismo worker maneja pipeline
comercial, contratos, finanzas, configuración fiscal por país,
provisioning cross-product y auditoría unificada.
Componentes principales
Sección titulada «Componentes principales»Layered structure (frontend)
Sección titulada «Layered structure (frontend)»src/client/├── App.tsx # Router + layout + clean-print routes├── pages/ # 23 pages│ ├── Login.tsx # Google OAuth│ ├── Dashboard.tsx # KPIs ejecutivos│ ├── Pipeline.tsx # Kanban leads│ ├── Tenants.tsx # Lista clientes con MRR + alertas│ ├── TenantDetail.tsx # Tabs: General, Fiscal, Contratos, Finanzas, Alertas│ ├── Organizations.tsx, OrganizationDetail.tsx│ ├── People.tsx, Directory.tsx│ ├── Contracts.tsx, Finance.tsx, Invoicing.tsx│ ├── FiscalConfig.tsx│ ├── PricingTool.tsx, ProductsCatalog.tsx│ ├── Services.tsx # Catálogo│ ├── product/ # Sub-pages producto: dashboard, issues, backlog,│ │ # journeys, modulos, tech, releases, roadmap│ ├── editorial/ # Posts + métricas│ ├── Checklist.tsx, Obligations.tsx│ ├── TimeTracking.tsx, VaultTeam.tsx│ ├── Wiki.tsx, Documents.tsx, Brand.tsx, BrandPrint.tsx│ ├── Infrastructure.tsx, Settings.tsx│ ├── Analysis.tsx, AuditLog.tsx│ ├── EmiAccessAdmin.tsx, EmiAnalytics.tsx│ ├── Assessments.tsx│ └── WellMetrics.tsx├── components/│ ├── Layout.tsx # Sidebar 7 grupos + topbar + collapsible│ ├── ds/ # IconBox · StatusDot · ThemeToggle│ ├── emi/ # EmiDrawer│ └── product/ # ProductSidebar · ReleaseCard · GanttChart · etc.├── lib/│ ├── auth.ts # Supabase auth wrapper│ ├── supabase.ts # Client pubic│ ├── api.ts # apiFetch helper (returns parsed JSON)│ ├── currency.ts # Format CLP/USD/UF│ └── markdown.ts # simpleMarkdown shared└── styles.css # DS 3 capas (primitives @theme + semánticos + dark)Layered structure (backend)
Sección titulada «Layered structure (backend)»src/├── index.ts # Hono app entry · Env type · mount order├── env.d.ts # Vite client env types├── api/ # 44 módulos│ ├── auth.ts # /auth/me · /auth/callback│ ├── tenants.ts # /api/tenants/* (provisioning + list)│ ├── internal-tenants.ts# /api/internal/tenants (X-Platform-Key)│ ├── clients.ts # /api/clients/* (DEPRECATED — usar tenants)│ ├── pipeline + leads + assessments + lead-emi-conversations│ ├── organizations + directory + team + vault-team + vault-permissions│ ├── contracts + finance + invoices + fiscal + mrr + exchange + obligations│ ├── catalog + products-catalog + pricing-tool│ ├── product.ts # 27 endpoints — products, layers, modules, features,│ │ # journeys, releases, backlog, gaps, GitHub sync│ ├── github-webhook.ts # webhook + cron horario│ ├── editorial + checklist + time + wiki + documents + infra + brand│ ├── audit + changelog + notifications + dashboard + kpis + analysis│ ├── ai.ts # Anthropic proxy — proposal · contract · summary│ ├── emi.ts # /api/emi/{chat,access} — proxy server-side│ ├── emi-access.ts # admin matrix tenant × app│ ├── emi-analytics.ts # KPIs + time series sobre emi_invocations│ ├── well-metrics.ts # 5 endpoints read-only sobre proyecto emawell│ └── quality.ts # Quality dashboard (3 tablas + 10 endpoints)├── middleware/│ ├── auth.ts # JWT + vault_users.is_active check│ └── security-headers.ts# HSTS · CSP · COOP/CORP · X-Frame-Options└── lib/ ├── supabase.ts # client emahub └── supabase-well.ts # client emawell separado (service role)Auth flow
Sección titulada «Auth flow»Provisioning de tenant (flow completo)
Sección titulada «Provisioning de tenant (flow completo)»EMI proxy (server-side)
Sección titulada «EMI proxy (server-side)»AI integrations (Anthropic)
Sección titulada «AI integrations (Anthropic)»src/api/ai.ts expone 3 endpoints server-side:
| Endpoint | Modelo | Uso |
|---|---|---|
POST /api/ai/proposal | Sonnet 4.6 | Genera propuesta comercial desde lead/cliente |
POST /api/ai/contract | Sonnet 4.6 | Resumen de contrato (lectura assistida) |
POST /api/ai/summary | Sonnet 4.6 | Resumen general (notas, descripciones) |
ANTHROPIC_API_KEY queda como secret CF Worker. Nunca expuesto al
SPA.
GitHub sync (product module)
Sección titulada «GitHub sync (product module)»CSP + security headers
Sección titulada «CSP + security headers»src/middleware/security-headers.ts aplica:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; img-src 'self' data: https://avatars.githubusercontent.com; connect-src 'self' https://*.supabase.co https://api.anthropic.com https://emi.emahealth.io https://api.github.com; frame-ancestors 'none';Plus: HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff, COOP same-origin, CORP cross-origin.
Audit trigger (cross-product)
Sección titulada «Audit trigger (cross-product)»Migration 008 instala emavault.audit_trigger() SECURITY DEFINER:
- Aplica a 12 tablas críticas (users, vault_users, organizations, contracts, finance, fiscal_config, invoices, products, etc.).
- Loggea cada INSERT/UPDATE/DELETE en
emashared.audit_log. - Sanitiza secrets: cualquier campo con nombres
password/token/secret/api_key/private_key/aidbox_client_secret_encrypted
se reemplaza por
[REDACTED]. auth.uid() = nullcuando se llama con service-role (esperado).
Patrones cross-cutting
Sección titulada «Patrones cross-cutting»- Sub-routes Hono — un archivo por módulo, registrado vía
app.route("/api/{resource}", routes)enindex.ts. - Public routes ANTES del authMiddleware —
/api/health,/api/config,/auth/*,/api/github/webhook,/api/internal/tenants. apiFetch<T>(...)helper retorna JSON parseado directamente (noResponse).- Imports relativos (
./,../) — no usa alias@/(diferente a Clinic/Lab). typesobreinterface— convención del repo.- Sub-pages lazy-loaded dentro del producto (
/producto/:slug/*).