Caso de estudio · 2026

expression.

Editor editorial colaborativo

La maquetación profesional vive en apps de escritorio para una sola persona. Pero una redacción es un coro. expression lleva el rigor de InDesign al navegador y le añade lo que falta: cada parte del pliego se reparte a su responsable, que edita solo lo suyo —sin ver el resto, ni siquiera por la red. De borrador a imprenta, con briefings, deadlines e historial siempre a la vista.

expression

Una redacción no es una persona

InDesign y Affinity son brillantes y profundamente mono-usuario: un archivo, un diseñador. Google Docs y Figma colaboran de maravilla, pero no saben de líneas base, medianiles ni pliegos. En medio queda el trabajo real de una revista —editores, redactores y diseñadores tocando partes distintas del mismo número, contra reloj— sin una sola herramienta que lo abrace.

Maquetación de escritorio, en una pestaña

expression es un editor de pliegos que corre en el navegador con la ambición de una herramienta de escritorio: páginas maestras, columnas con medianil, rejilla de línea base, estilos de párrafo y carácter, texto enriquecido e imágenes. Encima, el sistema nervioso de una redacción: briefings, deadlines, estados editoriales y reparto del trabajo por nodos. Sin instalar nada.

El editor de expression con un artículo maquetado
Guías de columna y línea base, frames de texto e imagen, isla de herramientas e inspector — en el navegador.
  • Pliegos con páginas maestras multipágina
  • Columnas, medianil y rejilla de línea base (6pt)
  • Texto enriquecido con estilos de párrafo y carácter
  • Multi-selección, grupos y smart guides con imantado
  • Efectos de capa: blur, ruido, modos de fusión
  • Compartir por nodo con roles (ver / comentar / editar)
  • Flujo editorial con estados, briefings y deadlines
  • Export PDF vectorial e historial con checkpoints

El truco: compartir sin filtrar

Compartir “solo una parte” suele ser una mentira piadosa: la interfaz esconde el resto, pero el documento entero viaja igual por la red. En expression, no. Cuando repartes un nodo, la frontera no está en la UI —está en la base de datos. Una función en Postgres construye un documento nuevo que solo contiene ese nodo; el invitado nunca recibe un byte de lo demás.

La seguridad no se pinta, se proyecta

Lectura y escritura pasan por funciones SECURITY DEFINER validadas por token: proyectan solo el nodo al leer y fusionan solo el nodo al guardar. Granularidad real, no permisos de pega.

-- el invitado solo recibe los frames de SU nodo; lo demás no viaja
v_doc := jsonb_build_object(
  'pages', (
    select jsonb_agg(jsonb_set(pg, '{frames}', (
      select jsonb_agg(fr)
      from jsonb_array_elements(pg->'frames') fr
      where fr->>'groupId' = g        -- ← aquí vive la frontera
    )))
    from jsonb_array_elements(d.doc->'pages') pg
  )
  -- + masters y estilos; nunca los otros nodos
);
La frontera, dentro de Postgres · resolve_share()

Del lado del cliente, el mismo editor guarda en tres destinos —dueño, documento completo o un solo nodo— eligiendo la función correcta. Toda la diferencia cabe en una línea:

const fn = target.kind === "shared-group"
  ? "save_shared_group"       // fusiona SOLO ese nodo
  : "save_shared_document";   // documento entero
await supabase.rpc(fn, { p_token: token, p_doc: doc });
El cliente solo elige la RPC · useDocSave.ts
Vista del invitado a un nodo compartido
Lo que ve el invitado: solo su nodo. El resto del pliego no existe del lado del cliente.
Vista de nodo en móvil
El mismo nodo en móvil: una columna, con corte exacto del texto sobrante.

Permisos que se sienten

Antes de repartir, el dueño puede “verse como” cualquier rol y aislar un nodo: un foco ilumina esa parte y funde a negro el resto del lienzo. El permiso deja de ser una casilla y se vuelve una experiencia —ves exactamente lo que verá tu colaborador.

Ultrafoco: aislar un nodo con un spotlight
Aislar nodo: el resto del lienzo se oscurece y desenfoca; “viendo como” el colaborador.

De borrador a imprenta

Cada nodo es una asignación con su briefing —ángulo, responsable, temas, deadline— y su estado. Los estados (Borrador → Revisión → Aprobado → Publicado) bloquean la edición cuando toca; el temporizador de cierre, siempre visible, cambia de color según aprieta la hora. El editor conoce el oficio, no solo las cajas.

Briefing del nodo en el inspector
El briefing de un nodo en el inspector: ángulo, responsable, temas, ideas y fecha de cierre.

Pulgadas, no píxeles

Un pliego no se mide en píxeles. expression trabaja en pulgadas y puntos (línea base de 6pt) y deriva toda la geometría —columnas, márgenes, imantado— de funciones puras. Mover una caja la pega a la columna y a la línea base. El detalle que lo hizo sentir “de verdad” fue imantar también al punto medio entre líneas.

// pegar a columnas y a la línea base es lo obvio.
// el matiz: imantar también al PUNTO MEDIO entre líneas base.
const mids = baselines
  .slice(0, -1)
  .map((y, i) => (y + baselines[i + 1]) / 2);
const snapY = [...baselines, ...mids];
El detalle que cambió la sensación · document.ts
800%
de zoom con geometría exacta
en pulgadas y puntos, derivada de funciones puras

El PDF es el editor, no una foto

Nada de rasterizar el lienzo. Una ruta de servidor abre la propia página de impresión con Chromium headless —reenviando tu sesión— y espera a que fuentes e imágenes estén listas antes de capturar. Sale un PDF vectorial, idéntico a la pantalla, listo para imprenta. El mismo código corre en tu Mac y en Vercel.

// solo cambia de dónde sale Chromium
return process.env.VERCEL
  ? puppeteer.launch({ args: chromium.args, executablePath: await chromium.executablePath() })
  : puppeteer.launch({ executablePath: systemChrome() });
// después: reenviar cookies, esperar fonts + imágenes, page.pdf()
Un solo código, dos entornos · api/pdf/[docId]/route.ts
PDF exportado de un número real
Un PDF real: texto justificado, guiones automáticos y folio — fiel al editor, vectorial.

Una marca que se dibuja sola

La identidad no es un logo estático: es un único trazo de curva continua —la “e”— que se anima dibujándose y morfa entre figuras del oficio: texto, página, pluma, lápiz, cursor. Vive como datos, no como imagen, así que el mismo gesto fluye en cualquier color y tamaño.

Lockup de marca: símbolo de trazo único + “expression”
El lockup: la “e” como un solo trazo de curva continua, junto al wordmark.
// no es un PNG: es un trazo abierto que morfa entre figuras
export const EXPRESSION = {
  name: "expression",
  d: "M398,560 C483,557 527,470 583,360 …",  // la "e", una curva
};
// texto · página · pluma · lápiz · cursor — un gesto, otra forma
La marca como datos · logoShapes.ts
Showcase de la marca generativa
El repertorio: el mismo trazo único, en movimiento, formando cada figura.
Paleta de color de la marca
Tinta, acento, identidad (el “viendo como”), guía base y papel cálido.

Cómo está hecho

Todo el documento es JSON puro, así que el editor se construyó local-first y el backend se conectó al final sin reescribir nada. Next.js 16 y React 19 al frente; Zustand + zundo para estado e historial; TipTap/ProseMirror para el texto rico; Supabase (Postgres, Auth, Storage y RPC) detrás; y Puppeteer para el PDF.

Framework
Next.js 16 · React 19
Estado
Zustand + zundo
Texto
TipTap / ProseMirror
Backend
Supabase · Postgres + RPC
PDF
Puppeteer + Chromium
Deploy
Vercel
  1. El modelo y el lienzo

    Documento como JSON puro + frames con drag/resize e imantado a columnas y línea base.

  2. Tipografía de imprenta

    Páginas maestras, estilos de párrafo y carácter, texto enriquecido con TipTap.

  3. Grupos y permisos como experiencia

    Multi-selección, grupos, simular rol y aislar un nodo con spotlight.

  4. El flujo de una redacción

    Estados que bloquean la edición, briefings por nodo y deadlines siempre visibles.

  5. Backend y nodos airtight ⭐

    Auth, persistencia, Storage y la proyección + merge scoped en la base de datos.

  6. PDF vectorial y marca generativa

    Export con Chromium headless y la identidad de trazo único que morfa.

No es una demo

expression nació como la herramienta interna de mi propia revista. Se ganó su sitio en producción —números reales, maquetados por un equipo real— antes de abrirse a la comunidad como SaaS, gratis por ahora. No es un prototipo buscando un problema: es un problema que ya tenía, resuelto.

Estado
En producción
Origen
Dogfooding → SaaS
Precio
Gratis (por ahora)
Dominio
expression.ululando.com

¿Construimos algo juntos?