Arquitectura
Esta página describe el stack de producción, el camino de un mensaje de chat, el modelo de base de datos y las fronteras de confianza. Es una inmersión técnica — lee la Visión general primero si quieres la foto a nivel de producto, o Conceptos de IA desde cero para el vocabulario de machine learning que la arquitectura implementa.
Los conceptos que esta arquitectura implementa
Sección titulada «Los conceptos que esta arquitectura implementa»Todo lo de abajo conecta las ideas del glosario de IA:
- El camino de request es una llamada LLM a través de un proxy. El cliente tiene la clave (BYOK), la envuelve en un sobre de handoff, y el servidor hace de proxy de
streamTexthacia el proveedor — así el bucle de tokens del modelo corre en el edge mientras tu clave nunca toca nuestro almacenamiento. - Local-first responde al problema del contexto. Como el modelo solo ve lo que se pone en su ventana, la plataforma debe re-suministrar memoria, documentos recuperados e historial en cada request — así que guarda los datos pesados (mensajes, código, diffs) localmente (IndexedDB/bridge SQLite) y solo un índice ligero en servidor. Lecturas offline rápidas, privacidad por defecto.
- La salida estructurada es un contrato de primera clase. Los planes y artefactos se recuperan del texto del modelo como objetos validados contra esquema (
Output.object/ Zod + escaneo tolerante), no como prosa libre, lo que permite que otras superficies (studio, artefactos) los consuman programáticamente. - Agentes / herramientas / MCP convierten a un único modelo en un sistema capaz de actuar sobre tus repos, dispositivos y servicios.
La tabla de stack y el camino de request de abajo son cómo esas ideas aparecen en tecnología concreta.
| Capa | Tecnología | Notas |
|---|---|---|
| Framework web | Astro 6 | Páginas marketing estáticas + rutas de servidor; islas para componentes interactivos |
| Routing cliente | TanStack Router (file-based) | El shell del dashboard (/space) usa un árbol de rutas construido a mano en DashboardApp.tsx; las páginas marketing usan rutas de Astro |
| UI | React 18+ (solo function components), Tailwind CSS v3, primitivas shadcn/ui, Radix | No hay class components en ningún sitio |
| Runtime | Cloudflare Workers | Stateless por isolate, sin fs, sin APIs solo-Node |
| Base de datos | Postgres (Supabase) vía Drizzle ORM | Migraciones generadas con drizzle-kit generate, nunca escritas a mano |
| IA | AI SDK v4 | streamText + Output.object({ schema }) para salida estructurada; generateText para no-streaming |
| Almacenamiento de medios | R2 (Cloudflare) | Assets de contenido y subidas |
| Estado | IndexedDB + localStorage (cliente) con vault cifrado | Híbrido local-first; índice ligero en Postgres para sync entre dispositivos |
| Auth | Magic-link + códigos OTP | src/lib/auth.ts / auth-server.ts |
| Pagos | LemonSqueezy | El webhook cambia users.tier entre free y paid |
Camino de una request de chat
Sección titulada «Camino de una request de chat»- Cliente lee el proveedor/modelo activos y la API key guardada del vault local cifrado (
credentials.ts). - El cliente construye un sobre de handoff de un solo uso: la clave en texto plano se envuelve para la clave pública E2EE del servidor con expiración corta (
buildVaultHandoffencredentials.ts→kdf.ts). - El sobre viaja a
POST /api/chat(src/pages/api/chat.ts). La ruta llama primero arequireResolvedAuth(cookies, request)— la única puerta de auth de todas las rutas API. - El servidor desempaqueta el sobre, resuelve el modelo del proveedor (
ai-providers.ts) y devuelve la respuesta en streaming constreamText(AI SDK v4). Las salidas estructuradas usanOutput.object({ schema }). - Cada mensaje se persiste en el servidor vía
chat-persistence.ts(Postgres) y en el cliente víalocal-chat-store.ts(IndexedDB). Los registros de uso (recordUsage) se escriben enusage_records. - El cliente renderiza el stream de forma incremental. Los artefactos y planes se extraen del texto del asistente en el cliente (
extractPlanFromContentenchat-helpers.ts).
¿Por qué local-first con índice ligero en servidor?
Sección titulada «¿Por qué local-first con índice ligero en servidor?»Los mensajes, el código y los diffs son pesados. Guardarlos solo en el navegador (IndexedDB) y en el bridge emparejado (SQLite) mantiene la app rápida y privada. Una fila de ~200 B por conversación en Postgres (conversations) basta para sincronizar títulos y marcas de tiempo entre dispositivos.
Modelo de base de datos (Postgres)
Sección titulada «Modelo de base de datos (Postgres)»El esquema Drizzle vive en src/lib/db/schema.ts. Tablas centrales:
| Tabla | Propósito |
|---|---|
users |
Identidad: email, nombre, tier (free/paid), preferencias (JSONB), flag de verificación |
sessions |
Sesiones de servidor (token + expiración) para llamadas API autenticadas |
verification_codes |
Códigos OTP por email con expiración y contador de intentos |
conversations |
Sesiones de chat: título, proveedor/modelo, árbol de forks (parentId, rootId, childCount), workspace ref (JSONB), nivel de thinking, contador de tokens, pinned/tags/status |
messages |
Mensajes por conversación: enum de rol, contenido JSONB, conteo de tokens, modelo, metadatos, columna tsvector de búsqueda |
folders |
Carpetas de conversaciones con color/icono/orden |
agents |
Catálogo de agentes: rol, descripción, system prompt, modelo/proveedor, tools (JSONB), categoría, tier de precio |
subscriptions |
Vínculos de suscripción LemonSqueezy (suscripciones de agentes) |
workflows, workflow_nodes, workflow_edges, workflow_runs |
Persistencia del pipeline studio |
content_assets |
Medios generados (imagen/vídeo/audio/3D) |
vault_entries, user_vault_config |
Sync opcional del vault en servidor (solo con opt-in explícito por proveedor) |
usage_records |
Métricas de tokens/coste por request: tokens prompt/completion/cacheados, reasoning, duración, modo, coste USD, tier |
devices, device_tokens, pairing_codes, device_action_log |
Emparejamiento de bridges, tokens de dispositivo, códigos de emparejamiento y el log de auditoría de consentimiento |
chunk_hashes |
Dedup por dirección de contenido para subidas |
task_outbox |
Patrón outbox para trabajos asíncronos |
Fronteras de confianza
Sección titulada «Fronteras de confianza»- Tus claves jamás salen de tu dispositivo salvo dentro de un sobre de handoff E2EE de corta vida, y solo durante la duración de la request que tú inicias. Esta es la promesa BYOK — ver Seguridad y BYOK.
- El daemon bridge (un repo Rust aparte) se conecta solo saliente vía WebSocket a la plataforma; no se abren puertos de entrada en tu máquina. Cada acción requiere consentimiento limitado a una capacidad (
desktop.shell.execute,desktop.fs.read, …). - Las rutas API están controladas por
requireResolvedAuth; CORS/rate limiting/redacción viven ensecurity.ts; los logs sensibles pasan por el único sumiderolog-redaction.ts.
Claves de almacenamiento cliente
Sección titulada «Claves de almacenamiento cliente»Todas las claves de persistencia del cliente llevan el prefijo synthhires:* (ver src/lib/nickname.ts para los helpers canónicos). Claves notables: synthhires-credentials (vault cifrado), synthhires-tokens (tokens de servicios), synthhires:activeConvId, synthhires:activeProvider, synthhires:enabledModels, synthhires:systemInstruction, synthhires:globalParams, synthhires:sidebar:width.
Convenciones que conviene saber
Sección titulada «Convenciones que conviene saber»- Los IDs son siempre
crypto.randomUUID()— nunca strings basados en timestamp (propensos a colisión con forks concurrentes). - Los eventos entre componentes usan sintaxis de dos puntos:
synthhires:conv:focus,synthhires:nickname:update,synthhires:sidebar:set. - Toda transición lleva
motion-reduce:transition-none motion-reduce:duration-0;transition-allestá prohibido.