expression.
Collaborative editorial editor
Professional layout lives in single-player desktop apps. But a newsroom is a choir. expression brings InDesign-grade rigor to the browser and adds what’s missing: each part of the spread goes to its owner, who edits only theirs —without seeing the rest, not even over the wire. From draft to print, with briefings, deadlines and history always in view.

A newsroom isn’t one person
InDesign and Affinity are brilliant and deeply single-player: one file, one designer. Google Docs and Figma collaborate beautifully, but know nothing of baselines, gutters or spreads. In between sits the real work of a magazine —editors, writers and designers touching different parts of the same issue, against the clock— with no single tool to hold it.
Desktop-grade layout, in a browser tab
expression is a spread editor that runs in the browser with the ambition of a desktop tool: master pages, columns with gutters, a baseline grid, paragraph and character styles, rich text and images. On top, the nervous system of a newsroom: briefings, deadlines, editorial statuses and work split into nodes. Nothing to install.

- ✓Spreads with multi-page master pages
- ✓Columns, gutters and a baseline grid (6pt)
- ✓Rich text with paragraph and character styles
- ✓Multi-selection, groups and smart guides with snapping
- ✓Layer effects: blur, noise, blend modes
- ✓Node sharing with roles (view / comment / edit)
- ✓Editorial flow with statuses, briefings and deadlines
- ✓Vectorial PDF export and history with checkpoints
The trick: share without filtering
Sharing “just one part” is usually a white lie: the UI hides the rest, but the whole document travels anyway. Not in expression. When you hand off a node, the boundary doesn’t live in the UI —it lives in the database. A Postgres function builds a brand-new document containing only that node; the guest never receives a single byte of the rest.
Reads and writes go through token-validated SECURITY DEFINER functions: they project only the node on read and merge only the node on write. Real granularity, not fake permissions.
-- 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
);On the client, the same editor saves to three targets —owner, full document or a single node— by picking the right function. The whole difference fits in one line:
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 });

Permissions you can feel
Before handing off, the owner can “view as” any role and isolate a node: a spotlight lights that part and fades the rest of the canvas to black. The permission stops being a checkbox and becomes an experience —you see exactly what your collaborator will see.

From draft to print
Each node is an assignment with its own briefing —angle, owner, topics, deadline— and its status. Statuses (Draft → Review → Approved → Published) lock editing when they should; the always-visible deadline timer shifts color as the hour tightens. The editor understands the craft, not just the boxes.

Inches, not pixels
A spread isn’t measured in pixels. expression works in inches and points (a 6pt baseline) and derives all geometry —columns, margins, snapping— from pure functions. Moving a box snaps it to the column and the baseline. The detail that made it feel real was snapping to the midpoint between lines, too.
// 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];The PDF is the editor, not a screenshot
No rasterizing the canvas. A server route opens the print page itself with headless Chromium —forwarding your session— and waits for fonts and images before capturing. Out comes a vectorial PDF, identical to the screen, print-ready. The same code runs on your Mac and on 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()
A brand that draws itself
The identity isn’t a static logo: it’s a single continuous-curve stroke —the “e”— that animates itself into being and morphs between figures of the craft: text, page, pen, pencil, cursor. It lives as data, not as an image, so the same gesture flows at any color and size.

// 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

How it’s built
The whole document is plain JSON, so the editor was built local-first and the backend wired in at the end without rewriting a thing. Next.js 16 and React 19 up front; Zustand + zundo for state and history; TipTap/ProseMirror for rich text; Supabase (Postgres, Auth, Storage and RPC) behind; and Puppeteer for the PDF.
- The model and the canvas
Document as plain JSON + frames with drag/resize and snapping to columns and baseline.
- Print-grade typography
Master pages, paragraph and character styles, rich text with TipTap.
- Groups and permissions as experience
Multi-selection, groups, role simulation and isolating a node with a spotlight.
- A newsroom flow
Statuses that lock editing, per-node briefings and always-visible deadlines.
- Backend and airtight nodes ⭐
Auth, persistence, Storage and the projection + scoped merge in the database.
- Vectorial PDF and generative brand
Export with headless Chromium and the morphing single-stroke identity.
Not a demo
expression was born as the in-house tool of my own magazine. It earned its place in production —real issues, laid out by a real team— before opening to the community as a SaaS, free for now. It’s not a prototype looking for a problem: it’s a problem I already had, solved.