El problema fundamental que Linear aborda es la latencia percibida en aplicaciones web interactivas, un desafío inherente a la arquitectura cliente-servidor tradicional donde cada interacción del usuario implica un viaje de ida y vuelta a la red. Históricamente, las aplicaciones web han luchado con la "sensación" de lentitud debido a la espera de respuestas del servidor.
Linear resuelve esto adoptando un modelo "local-first", donde el estado de la aplicación reside primariamente en el cliente (IndexedDB y memoria RAM), y las operaciones se ejecutan de forma optimista antes de la confirmación del servidor. Esto desacopla la reactividad de la interfaz de usuario de la latencia de la red, haciendo que las interacciones se sientan instantáneas. La sincronización con el servidor se convierte en una tarea de fondo, no un bloqueador de la UI. Este enfoque es particularmente relevante hoy en día, con la creciente demanda de experiencias de usuario fluidas y "nativas" en la web, y la ubicuidad de dispositivos con capacidades de almacenamiento y procesamiento local significativas.
Arquitectura del Sistema
La arquitectura de Linear se centra en un "sync engine" que invierte la relación tradicional cliente-servidor. El frontend, construido con React, TypeScript y MobX, utiliza IndexedDB como su base de datos principal en el navegador. Cuando un usuario realiza una acción (ej. actualizar un 'issue'), la mutación se aplica inmediatamente al estado local en memoria (MobX observable) y se encola en una cola de transacciones duradera en IndexedDB. La UI se actualiza de forma síncrona, proporcionando una respuesta instantánea al usuario.
Posteriormente, estas mutaciones se envían de forma asíncrona al servidor, que utiliza Node.js, PostgreSQL (con particionamiento), Redis (para bus de eventos y caché) y Kubernetes. El servidor valida la mutación y, si es exitosa, la persiste y la difunde como un "delta" a todos los clientes conectados a través de WebSockets. El cliente recibe este delta y actualiza el MobX observable correspondiente. La granularidad de MobX, donde cada propiedad es un observable, permite re-renderizados atómicos, actualizando solo los componentes específicos que dependen de la propiedad modificada, evitando re-renderizados en cascada. Para la carga inicial, se emplean técnicas como el "code splitting" agresivo (con Rolldown-Vite), "module preloading" en el <head/> del HTML, y un Service Worker para precache de rutas y assets, minimizando el JavaScript y CSS inicial y permitiendo capacidades offline. La autenticación también sigue un patrón optimista, asumiendo una sesión válida si hay datos locales y verificándola en segundo plano.
Flujo de Mutación Optimista
- 1 UI (Cliente) Usuario inicia una acción (ej. editar título de issue).
- 2 MobX (Cliente) Estado en memoria se actualiza inmediatamente. UI re-renderiza.
- 3 IndexedDB (Cliente) Mutación encolada en cola de transacciones duradera.
- 4 Sync Engine (Cliente) Mutación enviada asíncronamente al servidor.
- 5 Servidor (Backend) Valida, persiste la mutación en PostgreSQL.
- 6 Redis (Backend) Publica delta de cambio a través de bus de eventos.
- 7 WebSocket (Servidor) Delta transmitido a todos los clientes conectados.
- 8 MobX (Cliente) Cliente recibe delta, actualiza observable. UI re-renderiza atómicamente.
Flujo de Carga Inicial Optimizada
- 1 Navegador Solicita index.html.
- 2 Servidor/CDN Responde con index.html (CSS crítico y JS de arranque inlined).
- 3 Navegador Renderiza 'app shell' instantáneamente, inicia precarga de chunks vía <link r...
- 4 Navegador Ejecuta JS inlined: verifica localStorage para estado de sesión y tema.
- 5 Service Worker Instala y precachea chunks de rutas no críticas en segundo plano.
- 6 Navegador Ejecuta bundle principal (ya en caché por precarga).
- 7 IndexedDB (Cliente) App hidrata estado desde IndexedDB a MobX.
- 8 UI (Cliente) App completamente funcional, sin spinners, con datos locales.
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | IndexedDB | Base de datos principal en el cliente para persistencia local y offline. Almacena el estado completo del workspace. vs localStorage, Web SQL (deprecated), otros almacenes de datos en memoria sin persistencia Utilizado con un wrapper 'idb' para simplificar la API. |
| compute | Node.js | Runtime para todo el código del servidor, permitiendo un lenguaje único (TypeScript) end-to-end. vs Go, Java, Python, Ruby on Rails |
| storage | PostgreSQL | Base de datos relacional principal en el backend, usada como fuente de verdad. Las tablas de 'issues' están particionadas. vs MySQL, MongoDB, Cassandra Ejecutado en Cloud SQL, tablas de 'issues' particionadas en 300 formas. |
| messaging | Redis | Utilizado como bus de eventos para la sincronización de deltas entre clientes, caché y cursores de sincronización. vs Kafka, RabbitMQ, Google Pub/Sub Memorystore Redis en GCP. |
| orchestration | Kubernetes | Plataforma de orquestación de contenedores para desplegar y gestionar las cargas de trabajo del backend en GCP. vs Docker Swarm, AWS ECS, Google App Engine Una carga de trabajo por preocupación (one workload per concern). |
| networking | Cloudflare Workers | Proxy de borde multi-región para optimizar la entrega de contenido y la latencia global. vs AWS Lambda@Edge, Akamai EdgeWorkers |
| data-processing | turbopuffer | Base de datos vectorial para detección de 'issues' similares, probablemente para funcionalidades de búsqueda o recomendación. vs Pinecone, Weaviate, Qdrant |
| compute | React | Librería para construir la interfaz de usuario del frontend. vs Vue, Angular, Svelte |
| compute | MobX | Gestión de estado reactivo en el frontend, permitiendo actualizaciones granulares de la UI. vs Redux, Zustand, Recoil Utiliza observables para re-renderizados atómicos de componentes. |
| observability | Sentry | Monitoreo de errores para el frontend y backend. vs Datadog, New Relic, Rollbar |
Trade-offs
Ganancias
- ▲▲ Latencia percibida por el usuario
- ▲ Reactividad de la UI
- ▲ Capacidades offline
- ▲ Eficiencia de re-renderizado de UI
- ▲ Tiempo de carga inicial
Costes
- ▲ Complejidad del sync engine (cliente-servidor)
- △ Mayor uso de recursos del cliente (IndexedDB, memoria)
- △ Riesgo de inconsistencias temporales (eventual consistency)
- ▲ Esfuerzo de desarrollo inicial (sync engine custom)
// A traditional web app updating the server
async function updateIssue({ issue }) {
showSpinner();
const response = await fetch(`/api/issues/${issue.id}`, {
method: "PATCH",
body: JSON.stringify({ title: issue.title }),
});
const updated = await response.json();
setIssue(updated)
hideSpinner();
}
// vs Linear
issue.title = "Faster app launch";
issue.save();export default defineConfig({
plugins: [react()],
build: {
target: "esnext", // no legacy syntax, no polyfills
cssMinify: "lightningcss",
modulePreload: { polyfill: false },
rollupOptions: {
output: {
// One chunk per npm package > ~3 KB. Cache invalidation
// becomes per-library instead of per-app-revision.
manualChunks(id) {
if (id.includes("node_modules")) {
const pkg = id.match(/node_modules\/([^/]+)/)?.[1];
if (pkg) return `vendor-${pkg}`;
}
},
},
},
},
});<script type=module crossorigin
src="https://static.linear.app/client/assets/html.2_JBQs3Q.js"></script>
<link rel=modulepreload crossorigin
href="https://static.linear.app/client/assets/vendor-mobx.Crhy2qQc.js">
<link rel=modulepreload crossorigin
href="https://static.linear.app/client/assets/SyncWebSocket.Djw6l_Op.js">
<link rel=modulepreload crossorigin
href="https://static.linear.app/client/assets/DatabaseManager.DKssGAN8.js">Fundamentos Teóricos
El concepto de "local-first software" y la aplicación optimista de mutaciones tienen raíces en la investigación sobre sistemas distribuidos y tolerancia a fallos. Los "Conflict-free Replicated Data Types" (CRDTs), aunque no mencionados explícitamente como la implementación del sync engine de Linear, representan una clase de estructuras de datos que permiten la edición concurrente y la fusión automática de cambios sin conflictos, un principio fundamental para sistemas que operan localmente y sincronizan asíncronamente. El paper "A comprehensive study of Convergent and Commutative Replicated Data Types" de Marc Shapiro et al. (2011) es un trabajo seminal en esta área.
La idea de la "eventual consistency" también es central, donde el sistema garantiza que, si no hay nuevas actualizaciones, todas las réplicas eventualmente convergerán al mismo estado. Esto se alinea con el modelo de Linear donde el servidor es la fuente de verdad para la corrección, pero el cliente opera con su estado localmente consistente. El uso de un "Write-Ahead Log" (WAL) implícito en la cola de transacciones de IndexedDB para durabilidad local es un patrón clásico de bases de datos para garantizar la atomicidad y recuperación de transacciones, como se describe en "The R* Optimizer: A Unified Architecture for Dynamic Data Access" de Selinger et al. (1979) para sistemas de bases de datos relacionales.