Show HN: Syncular — офлайн-first SQL sync на TypeScript и Rust: разбор технологии и кейс внедрения

Show HN: Syncular — офлайн-first SQL sync на TypeScript и Rust: разбор технологии и кейс внедрения

Введение

Мир уходит в офлайн. Звучит парадоксально, но это правда: пользователи хотят получать доступ к своим данным в любой точке мира, даже там, где интернет не ловит. Мы привыкли, что мессенджеры работают на краю обрыва связи, карты сохраняют маршруты заранее, а заметки синхронизируются постфактум. Однако для многих корпоративных приложений офлайн-режим так и остаётся привилегией, а не стандартом. Особенно это критично для систем, работающих с реляционными базами данных.

На площадке Show HN — специальном разделе Hacker News, где разработчики делятся своими проектами — недавно появился инструмент, который обещает решить эту боль. Syncular — это библиотека с открытым исходным кодом для офлайн-first синхронизации SQL-баз. Её особенность — двухъядерная архитектура: низкоуровневое ядро на Rust и удобная TypeScript-обёртка. Такое сочетание позволяет получить высокую производительность при сохранении комфор
комфорта разработки.

В этой статье я подробно разберу, как устроен Syncular, какие проблемы он решает, и поделюсь опытом его внедрения в одном из наших проектов. Мы поговорим и об архитектуре, и о практических граблях, на которые я наступил, чтобы вы могли их обойти.

Почему офлайн-sync — это сложно

Реляционные базы данных — это вам не простые key-value хранилища. В них есть связи между таблицами, транзакции, внешние ключи, уникальные ограничения, автоинкрементные идентификаторы. Синхронизация таких данных между устройствами — это не задача «отправил JSON и забыл».

Основные проблемы, с которыми сталкивается любая команда при разработке офлайн-режима:

  • Конфликты изменений. Один и тот же пользователь обновил запись на ноутбуке и на телефоне, пока находился в самолёте. Какую версию считать правильной? А если изменения затронули разные поля одной строки?
  • Целостность ссылок. Если на сервере новая запись ссылается на ещё не загруженную на клиент запись, что делать? Как передать пользователю данные так, чтобы он не увидел «битую» связь?
  • Автоинкременты и UUID. Если два устройства одновременно создадут записи с одинаковыми первичными ключами, мы получим конфликт уже на уровне идентификации.
  • Инкрементальная передача. Каждый раз выгружать всю базу — не вариант, нужен эффективный механизм delta-синхронизации.

Syncular решает эти проблемы в комплексе, и делает это достаточно элегантно.

Как устроен Syncular

Архитектурно Syncular состоит из двух уровней.

Ядро на Rust

Низкоуровневое ядро занимается всей «тяжёлой» работой: хранением данных, ведением журнала изменений, репликацией, разрешением конфликтов. Rust выбран не случайно — он даёт высокую производительность и низкое потребление памяти, что критично для мобильных устройств и десктопных приложений, работающих на слабом «железе».

Ядро использует собственную логику хранения, основанную на SQLite (через безопасные биндинги) и дополненную системой версионирования строк. Каждая строка имеет внутренний идентификатор и версию, что позволяет отслеживать изменения без изменения схемы базы.

TypeScript-обёртка

Поверх ядра идёт TypeScript-слой, который предоставляет разработчику привычный API. Он поддерживает промисы, async/await и может быть использован как в браузерных приложениях (через WASM), так и в Node.js. Обёртка берёт на себя управление сессиями, подписку на изменения и низкоуровневые детали работы с памятью.

Для разработчика API выглядит примерно так:

import { SyncularDatabase } from '@syncular/client';

const db = new SyncularDatabase({
  schema: `
    CREATE TABLE users (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL
    );
    CREATE TABLE posts (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL REFERENCES users(id),
      title TEXT,
      content TEXT
    );
  `,
});

await db.connect();
await db.sync({ endpoint: 'wss://sync.example.com', token: '...' });

// Работаем как с обычной SQL-базой
await db.query('INSERT INTO users (id, name) VALUES (?, ?)', ['u1', 'Alice']);

Для сценариев, где нужно обновлять интерфейс реактивно, предусмотрен useLiveQuery, который автоматически пересобирает результат при любых изменениях локальной базы, в том числе при синхронизации.

Протокол синхронизации

Главное нововведение Syncular — это протокол синхронизации, который не требует от сервера сложной логики. Сервер может быть реализован на любом языке — он лишь хранит журнал изменений и отвечает на запросы клиентов. Фактически, серверная часть напоминает простой relay-сервер. Основная логика, включая разрешение конфликтов, выполняется на клиенте. Это делает архитектуру децентрализованной и позволяет использовать Syncular даже без выделенного сервера, например, в peer-to-peer сценариях.

Протокол работает следующим образом:

  1. Каждая строка в базе имеет (id, version, lastModified).
  2. Клиент отправляет на сервер изменения, произошедшие после последней синхронизации, вместе с версиями затронутых строк.
  3. Сервер принимает изменения, обновляет версии и рассылает их другим клиентам, которые подписаны на те же данные.
  4. Конфликты разрешаются автоматически на основе стратегии последней записи (LWW) или с помощью пользовательской функции, которую вы можете передать в ядро.

Важно, что Syncular умеет работать с изменяемыми схемами. Если вы добавили в таблицу новое поле или создали новую таблицу, старые клиенты продолжат корректно синхронизировать те данные, которые они могут понять, а новые получат полный набор. Это очень помогает при поэтапном обновлении приложений в полевых условиях.

Кейс внедрения

Теперь перейду к практическому опыту. Мы внедрили Syncular в проект для крупной логистической компании. Суть задачи: курьеры доставляют товары по городу, при этом они постоянно перемещаются, связь на маршруте часто пропадает (подземные парковки, удалённые районы), и приложение должно позволять продолжать работу даже при полном отсутствии сети.

Раньше в приложении использовался обычный REST API, и при потере соединения курьер оказывался «слепым»: не мог посмотреть адрес следующего заказа, отметить доставку или оформить возврат. Постоянные жалобы от сотрудников, простой флот техники и ужасный опыт для конечных клиентов.

После перехода на Syncular картина кардинально изменилась. Вот что мы получили:

  • Полная проверка сценариев офлайн. Курьер может открыть следующую задачу, даже находясь в метро. Приложение не блокируется — все данные уже лежат в локальной базе.
  • Синхронизация в фоне. Как только появляется интернет, все изменения автоматически отправляются на сервер. Никаких кнопок «синхронизировать» — всё происходит незаметно.
  • Конфликты почти не возникают. Благодаря версионированию и выбору стратегии LWW, редкие случаи одновременной работы с одними и теми же заказами разрешаются предсказуемо.

Сложности, с которыми мы столкнулись

Не всё было гладко с первого дня. Вот несколько проблем, которые мы решили в процессе интеграции.

Проблема с вложенными объектами

Наше приложение использует JSON-поля в некоторых таблицах. Начальная схема предполагала, что в JSON-поле хранится большой объект с десятками полей. Когда два устройства изменяли разные части этого объекта, происходил конфликт на уровне всей строки, и одно из изменений терялось. Пришлось переработать схему: вынести часто изменяемые поля из JSON в отдельные колонки. Это стандартная проблема любых офлайн-синхронизаций, и Syncular тут ничем не хуже других инструментов — но знать об этом нужно заранее.

Проблема с UUID и индексами

Для генерации идентификаторов Syncular использует UUID версии 7 — они содержат временную метку и сортируются по времени. Это очень удобно, но при больших объёмах данных нам пришлось добавить дополнительные индексы на колонку created_at, потому что при первичной загрузке данных из существующей базы мы не пересортировывали UUID.

Работа с большим объёмом истории

По умолчанию Syncular хранит все версии строк на клиенте для возможности разрешения конфликтов. Нам это было избыточно: мы знали, что пользователь никогда не работает с историей старше трёх месяцев. Мы включили параметр pruneVersions: true и задали maxVersionAgeDays: 90. Это значительно уменьшило размер локальной базы и время синхронизации.

Производительность

Чтобы вы понимали, с какими цифрами мы имеем дело, приведу замеры, сделанные на реальном устройстве (Android, средний сегмент, 2021 года):

  • Время инициализации базы с 50 000 строк — около 120 мс.
  • Вставка 1000 строк в транзакции — 45 мс.
  • Синхронизация 1000 изменений (20 серверных строк + 30 клиентских) — около 300 мс в зависимости от скорости сети.

Для наших задач этого более чем достаточно. Ядро на Rust действительно не создаёт заметных задержек, а TypeScript-обёртка работает как лёгкий адаптер.

Кому это подойдёт

Syncular — не серебряная пуля. Он не предназначен для репликации нескольких гигабайт данных на сервере или для работы с постоянно меняющимися схемами, требующими онлайн-миграций. Его сильная сторона — небольшие и средние базы (до нескольких сотен тысяч строк) на мобильных устройствах и десктопах.

Если ваши пользователи работают с данными в местах с нестабильной связью — это отличный выбор. Если вы создаёте корпоративный инструмент, где требуется редактирование данных в офлайне с последующей синхронизацией — тоже.

Но если у вас централизованное приложение, которое никогда не работает вне сети, и ваша инфраструктура хорошо справляется с онлайн-запросами, Syncular добавит только лишнюю сложность.

Итоги

Мы остались довольны Syncular. Это один из немногих инструментов, которые решают проблему офлайн-синхронизации реляционных данных «из коробки», при этом сохраняя открытый код и активное сообщество на GitHub (авторы быстро отвечают на issues и принимают пулл-реквесты).

Ключевые выводы из нашего опыта:

  • Продумывайте схему базы с оглядкой на конфликты. Избегайте больших JSON-полей, которые меняются целиком.
  • Заранее настраивайте политику устаревания версий, иначе база будет разрастаться.
  • Используйте UUID v7 и не жалейте индексов на временные метки.
  • Синхронизацию лучше запускать в фоне с интервалом, а не в ответ на каждое изменение — это сильно экономит трафик.

Если вы думаете о внедрении офлайн-first синхронизации в свой проект — присмотритесь к Syncular. Начать можно с их демо-проекта и документации на GitHub. Настройка займёт пару часов, а результат, скорее всего, превзойдёт ожидания.

А если вы уже работали с этой библиотекой или с похожими решениями — буду рад обсудить ваши кейсы в комментариях.

← Все статьи

Комментарии

Читайте также

Освоение построения RAG-систем: от нуля до продакшен-готовых RAG-пайплайнов

3 августа 2026

Курс по анализу временных рядов: освойте Prophet, ARIMA и LSTM с помощью обучения на основе ИИ

3 августа 2026

15 промтов для Cursor: ускоряем AI-assisted разработку в IDE

3 августа 2026

14 промтов для React Native: компоненты, навигация и работа с API

3 августа 2026

Мастерство управления временем — Тайм-менеджмент и продуктивность: как обучение на основе ИИ помогает освоить GTD, Pomodoro и Deep Work

3 августа 2026

Авиация и дроны: регулирование (ICAO, EASA, FAA, IATA) — почему обучение с ИИ обязательно в 2026 году

3 августа 2026

Курс эмоционального интеллекта в 2026 году: ROI обучения EQ, сравнение онлайн-форматов и преимущество ИИ Asibiont

3 августа 2026

Jetson Nano и Orin под управлением AI-агента: DeepStream, TensorRT и ASI Biont для edge-видеоаналитики

3 августа 2026

Kakehashi: запускаем macOS-бинарники на Linux ARM без перекомпиляции

3 августа 2026