Я начал пользоваться платформой Linear пару месяцев назад. То, с чем я столкнулся, затянуло меня в кроличью нору деталей local-first-разработки (локально-ориентированной разработки), которые изменили мой взгляд на веб-приложения.

Для тех, кто не в курсе, поясню. Linear — это платформа для управления проектами, которая, когда с ней работаешь, кажется невероятно, невозможно быстрой. Щёлкаешь по задаче — она мгновенно открывается. Меняешь какой-нибудь статус и наблюдаешь за этим из другого браузера — в нём всё обновляется почти так же быстро, как и там, где вносят изменения. Никаких индикаторов загрузки, никаких обновлений страниц — всё происходит ну просто моментально.
После того, как я много лет разрабатывал традиционные веб-приложения, это казалось чем-то противоестественным. А где сетевые задержки? А как разрешаются конфликты? А что происходит, когда программа уходит в оффлайн?
Если вы пока ещё не вполне понимаете — что такое local-first-приложения, то, полагаю, лучше всего вам это покажет демка LiveStore.
Добро пожаловать в кроличью нору
Когда судьба подарила мне дождливые выходные и целое море кофе — я наполнился решимостью разобраться во всей этой магии. Мне удалось обнаружить целый кладезь серьёзных технических материалов:
Реверс-инжиниринг движка синхронизации Linear — эти материалы получили высокую оценку CTO компании.
Ещё один похожий материал — подробный разбор их протокола синхронизации.
Выступление CTO Linear Туомаса Артмана, посвящённое архитектуре их системы.
Продолжение выступления, где речь идёт о масштабировании движка синхронизации.
Статья Figma об их технологии организации совместной работы, которую упоминал Туомас.
Если рассказать об этом в двух словах, то получается, что компания создала собственный движок синхронизации, который относится к IndexedDB браузера как к реальной базе данных. Каждое изменение сначала происходит локально, а потом, в фоне, для работы с мутациями применяется GraphQL, а для синхронизации данных — WebSocket-соединения.
Ещё — мне постоянно попадался термин «local-first», который, в зависимости от того, что именно я читал, относился к разным вещам. Во-первых — это могла быть UX-стратегия разработки приложений, которые воспринимались пользователями как локальные (мгновенные обновления данных и прочее подобное). Во-вторых — это могла быть философия разработки, в рамках применения которой данные хранятся локально и синхронизируются между устройствами.
Чаще всего идеи локально-ориентированной разработки выглядели до смешного простыми. Вместо того, чтобы превращать приложение в затейливую веб-форму, которая отправляет данные на сервер, ему дают собственную локальную базу данных. Иногда сервер — это всего лишь другой клиент, с которым синхронизируют данные. Это способно в корне перевернуть типичный подход к созданию веб-приложений.
В традиционном веб-приложении единственным достоверным источником данных является сервер:
Клиент → HTTP-запрос → Сервер → База данных → Ответ → Клиент
При применении подхода local-first с использованием синхронизации у каждого из клиентов может иметься собственная (почти) полная копия базы данных:
Клиент → Локальная база данных → Обновление пользовательского интерфейса
↓ (асинхронные действия)
Движок синхронизации → Сервер → Другие клиенты
Самым главным для меня здесь стало то, что, перемещая базу данных на сторону клиента, мы убираем сетевые задержки из цепочки взаимодействия пользователя и программы. Обновления интерфейса происходят мгновенно, так как они, по сути, сводятся к операциям чтения/записи, направленным на локальную базу данных.
Сложности, с которыми я столкнулся
После того, как я разобрался с сутью подхода, используемого Linear, первым моим желанием было — взять и сделать что-нибудь похожее. А потом на меня обрушилась реальность: даже простейшая версия их движка синхронизации — это проект, на который, вероятно, уйдут месяцы.
Сложность подобного проекта кроется в следующих деталях:
Корректная обработка переходов между оффлайновым и онлайновым режимами работы.
Разрешение конфликтов между распределёнными клиентами.
Частичная синхронизация (никого не порадует необходимость загрузки всей базы данных).
Миграции схемы при работе с кэшированными данными.
Информационная безопасность и контроль доступа в распределённой системе.
Уверен — кто-то уже собрал воедино все эти чудеса, сделав из них что-то такое, чем можно пользоваться.
Экосистема локально-ориентированной разработки в 2025 году
К счастью, сообщество любителей local-first-решений не сидит без дела. Вот проекты, которые подходят для использования в продакшне:
Electric SQL — движок синхронизации, основанный на Postgres.
PowerSync — решение, ориентированное на корпоративные системы.
Jazz — проект, который меня заинтересовал (ниже его обсудим).
Replicache — проект-ветеран (его больше не развивают).
Zero — новый проект от команды, создавшей Replicache.
Triplit — синхронизация, основанная на TripleStore.
Instant — проект, нацеленный на удобство разработки.
LiveStore — реактивный слой для Electric и других провайдеров.
Подробный разбор Jazz
Я начал именно с Jazz из-за того, что разработчики этого проекта сделали абсурдное в своей смелости заявление: создание local-first-проектов — это так же просто, как обновление локального состояния приложений.
Ментальная модель
Jazz вводит в обиход понятие «Collaborative Values» (CoValues, коллаборативные значения). Это — структуры данных, спроектированные для организации совместной распределённой деятельности в реальном времени.
Работа начинается со схемы, созданной с помощью Jazz и Zod:
// schema.ts
import { co, z } from "jazz-tools";
const ListOfComments = co.list(Comment);
export const Post = co.map({
title: z.string(),
content: z.string(),
comments: ListOfComments,
});
Мощь этого кода в том, что перед нами — не просто определение типов. Это — рабочие реактивные объекты, которые синхронизируются автоматически.
Рассмотрим пример:
// Используем хук подписки для получения значения
const post = useCoState(Post, postId)
// После этого пользуемся им как обычным объектом
const setTitle = (title: string) => {
post.title = title
// Вот и всё. Данные синхронизированы. И это не шутка.
}
Не нужно никаких API-маршрутов. Нет циклов запросов/ответов. Просто… объекты, которые волшебным образом синхронизируются. Когда этим пользуешься, возникает такое ощущение, будто играешь в игру с чит-кодами.
Как Jazz этого добивается
В недрах Jazz используется несколько хитроумных механизмов.
1. Встроенные средства для обеспечения уникальности объектов
Каждой сущности автоматически назначается уникальный ID. Это позволяет избегать коллизий и даёт возможность эффективно синхронизировать данные.
2. Применение шаблона Event Sourcing
Похоже, что изменения хранятся в виде событий, а полный граф объектов строится на основе этих событий. Благодаря этому операции синхронизации выполняются быстро, так как синхронизировать нужно только изменения.
3. Сквозное шифрование
Данные шифруются на клиенте до синхронизации. Сервер видит лишь зашифрованные объекты. С архитектурной точки зрения это восхитительно… но применение такого подхода вызывает определённые сложности, о которых я расскажу ниже.
4. Модель разрешений, основанная на группах
Вместо традиционных ACL в Jazz используются группы:
const group = Group.create()
group.addMember(alice, 'admin')
group.addMember(bob, 'writer')
Post.create(
{ title: "a new post"},
{ owner: group }
);
Подводные камни
Разработка приложений с использованием описанной архитектуры отличается чрезвычайной продуктивностью, в частности — при создании прототипов. Здесь нет привычных отвлекающих факторов, когда останавливают работу над пользовательским интерфейсом и идут писать операции API или настраивать DTO для каждого действия.
Всё это, конечно, хорошо, но при применении Jazz обнаруживаются и некоторые неприятности.
Сервер ничего не видит
Всё в Jazz-системе защищено сквозным шифрованием. Это значит, что бэкенд, в буквальном смысле слова, не может читать пользовательские данные, ну — только если ему в явном виде не предоставлено на это разрешение через механизм групп. Это, с точки зрения приватности, просто замечательно. Но всё уже далеко не так приятно, если заранее не подумать над тем, с какими данными может понадобиться работать серверу. Это — проблема и в том случае, если в системе, основанной на Jazz, нужно организовать модерацию контента, или сделать так, чтобы в ней нельзя было бы хранить вредоносные данные.
Путешествия во времени неизбежны
Похоже, что в Jazz используется шаблон Event Sourcing. Каждое изменение хранится в системе вечно. Имеется кнопка «Удалить»? Она просто убирает ссылки. Это хорошо подходит для реализации механизма «Отменить/Повторить». А вот если подумать о чём-то вроде соответствия системы GDPR, то это уже далеко не так хорошо.
Хранилище делает брррр
Так как ничего в системе не удаляется, оказывается, что размер используемого хранилища данных меняется только в одном направлении: в сторону роста. Делаете маленький проект? Да какая разница, в конце концов. А как насчёт SaaS с тысячами пользователей? При таком подходе счёт за услуги AWS может стать похожим на телефонный номер (или счёт за использование Jazz Cloud, когда компания предложит платные услуги).
Локальная разработка, всё же, не обходится без неожиданностей
Самым первым методом аутентификации, который предлагается использовать в Jazz-приложениях, являются ключи Passkeys. У этих ключей много плюсов, но их использование в локальной разработке может быть связано с определёнными сложностями.
Я создал на своём ноутбуке маленькое приложение и хотел протестировать его на телефоне, используя локальный IP-адрес. Вот какие у меня получились «путевые заметки»:
А, ведь ключам Passkeys, когда работают не на localhost, нужен HTTPS.
Включаем HTTPS в Vite через mkcert.
Ага, тут нужен доверенный сертификат.
Возьму-ка я Clerk, штука не идеальная для self-hosting-приложения, но ничего, сойдёт.
Так, а как перенести криптографические ключи пользователя Jazz в Clerk?
Clerk тоже, оказывается, нужен HTTPS, когда работают не на localhost, ну — справедливо.
Снова запускаем mkcert.
Ну правда — WebSocket для системы синхронизации Jazz тоже надо защитить.
Хорошо — всё проксируем.
прошло очень много времени… Заработало!
В общем — я обратил внимание на то, что в Jazz ожидается появление интеграции с Better Auth, что решит проблемы с аутентификацией в self-hosting-проектах.
А если честно? Оно того стоит
Несмотря на вышесказанное, Jazz — это проект, который произвёл на меня огромное впечатление. На его основе приятно и интересно создавать что-то своё. Он даёт программисту уникальный опыт разработки и способствует высокой производительности труда. Надо отметить, что сейчас — начало жизненного пути Jazz, и я уверен в том, что многие из тех проблем, с которыми я столкнулся, в будущем исчезнут.
Исследование: Electric SQL и Zero
Следующими в списке проектов, которые мне хотелось изучить, идут Electric SQL и Zero. В то время как создатели Jazz вызывают к жизни нечто совершенно новое, Electric и Zero сильнее опираются на то, что уже было:
-- Просто, как обычно, создаём таблицы Postgres
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
author_id INTEGER NOT NULL,
);
В случае с Electric потом можно использовать реактивные запросы, позволяющие получать подмножества базы данных (называемые Shapes). В этом примере настраивается подписка, которая позволяет Electric синхронизировать любые будущие изменения с пользовательским интерфейсом.
import { useShape } from '@electric-sql/react'
// С применением реактивных запросов
function Component() {
const { data } = useShape({
url: `http://localhost:3000/v1/shape`,
params: {
table: `posts`
}
})
return (
<pre>{ JSON.stringify(data, null, 2) }</pre>
)
}
Подход, применяемый в Electric, кажется мне привлекательным, учитывая то, что этот проект работает с существующими базами данных Postgres. Но, тут остаётся один открытый вопрос: как быть с мутациями? Чтобы при работе с Electric достичь продуктивности, сравнимой с Jazz, интересной кажется перспектива «прикручивания» к Electric чего-то вроде LiveStore. Правда, этот шаг повлечёт за собой необходимость применения особенных требований к схеме для Postgres DB. В качестве альтернативы тут может выступить TanStack DB, в ближайшем будущем я планирую попробовать этот проект.
Ещё один вариант — применение Zero. Этот проект очень похож на Electric, но он, кроме того, напрямую поддерживает мутации.
Ну, что бы я ни выбрал, об этом я уже, скорее всего, расскажу в одном из следующих материалов.
Когда имеет смысл прибегать к local-first-разработке?
После того, как я ознакомился с идеями local-first-разработки и поэкспериментировал с Jazz, у меня возникли следующие мысли по поводу того, в каких ситуациях этот подход к разработке может оказаться очень кстати.
Итак, local-first-разработка отлично подходит для следующих проектов:
Инструменты для творчества (дизайн, тексты, музыка).
Приложения для совместной работы или соответствующие элементы более крупных приложений.
Мобильные приложения, которым необходимо сохранять работоспособность в оффлайн-режиме.
Инструменты для разработчиков.
Приложения для повышения личной продуктивности.
А вот — ситуации, в которых применение локально-ориентированной разработки может столкнуться с определёнными трудностями:
Обеспечение работы мощной серверной бизнес-логики.
Проекты, которые обязаны соответствовать неким строгим правилам.
Крупномасштабные аналитические системы.
Существующие системы, которые глубоко интегрированы с другими системами.
Системы, в которых запросы к серверам регулярно отклоняются серверной логикой (это усложняет оптимистичные обновления данных).
Что дальше?
Локально-ориентированная разработка представляет собой фундаментальный сдвиг в том, как мы подходим к созданию приложения. Применение таких приложений значительно улучшает пользовательский опыт взаимодействия с ними. Доказательство тому — Linear. Целесообразность применения local-first-технологий в конкретном проекте определяется балансом плюсов и минусов, связанных с этими технологиями.
Я занимаюсь разработкой личного приложения, основанного на Jazz, стремясь понять эти плюсы и минусы на практике. Процесс разработки даёт мне массу свежих ощущений, отличающихся от того, что я испытывал раньше. Но я, при этом, внимательно наблюдаю за происходящим, ожидая ситуаций, в которых то, что скрыто за удобными и с виду простыми абстракциями, вдруг из-за них выглянет.
Экосистема local-first-разработки ещё очень молода. Инструменты этой экосистемы будут постепенно взрослеть, возникнут проверенные временем шаблоны программирования, будут сглажены «острые углы». Но самое главное никуда не денется: локальное хранение данных — это путь к UX, который гораздо лучше чем тот, к которому мы привыкли.
Если вы создаёте что-то новое и способны работать, соблюдая определённые ограничения, я очень советую вам попробовать локально-ориентированную разработку. В худшем случае вы просто освоите новый архитектурный шаблон. А в лучшем — создадите что-то такое, что покажется тем, кто будет этим пользоваться, нереально быстрым.
Это — весомый плюс в мире, где все привыкли к 300-миллисекундным паузам между отправкой запросов и получением ответов.
О, а приходите к нам работать? 🤗 💰
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.