clientful

clientful

Как разрабатывать многопользовательские приложения без серверов и баз данных Вместо: Client + Backend + DB + Cache + Files storage + Realtime Layer + Kubernetes + Servers Нужны всего-лишь: Client + Files storage + Realtime Layer
 
 

От автора

notion image
Привет, дорогой читатель, меня зовут Давид Шекунц и я Tech Lead с 12-летним опытом (а еще рассказываю про разработку в 🦾 IT-Качалке 💪)
clientful – это не устоявшийся термин, я его использую здесь, потому что он лучше всего описывает то решения, о которых я здесь пишу.
Этот подход уже демонстрирует свою работоспособность и открывает много не только технологических, но и бизнес-преимуществ при разработке проектов.
Этой работой я хочу поделиться с вами своим опытом, принципами и трюками, которые я обнаружил для того, чтобы создавать полноценные SaaS-like приложения без backend.
 

i. Вводные

Вот условия, которые мы хотим соблюсти, при этом отказавшись от серверных приложений и баз данных:
 
  1. Вся бизнес-логика и все вычисления должны происходить на клиенте
  1. Вся работа с БД должны происходить на стороне клиента
  1. Система должна поддерживать работу со множества устройств одного пользователя
  1. Cистема должна быть многопользовательской
  1. Система должна также уметь хранить файлы (фото, видео, аудио, pdf, etc.)
  1. Система должна быть безопасной
 
Чтобы “без backend-а” достичь этих условий нам потребуются:
 
  1. File Storage – внешнее файловое хранилище* S3-like, Google Drive-like или IPFS-like сервисы для персистенции и передачи данных и файлов между устройствами и пользователями. При этом, пользователь может использовать свои собственные хранилища без необходимости нам предоставлять свой backend.
  1. Web Socket-like Broker для маршрутизации сообщений между клиентами. Особенность clientful реализации позволит использовать абсолютно любой сервис, который даст возможность подключатся к комнате и бродкастить сообщения всем подключенным пользователям
 
* – есть варианты, когда можно отказаться от внешнего файлового хранилища, сохранив возможность коллаборации, НО среднестатистически реализация будет сильно сложнее, поэтому они имеют смысл только в каких-то очень узких кейсах.
 

ii. Преимущества

Что вы получаете из коробки:

a. Бизнес

 
  1. 0 стоимость инфраструктуры – для некоторых бизнесов стоимость хранения данных пользователей и обработки их запросов – непреодолимый барьер из-за которого они не успевают дожить до окупаемости. В clientful все вычислительные ресурсы и системы хранения полностью отданы на сторону пользователя, а значит вам вообще не придется платить за сервера, приложения, базы данных, кэши и подобное.
  1. Скорость интеграции – вся система представляет собой набор из клиентского приложения (например, html, css, js), S3-like хранилища и WS сервера. Это позволяет крайне быстро интегрировать подобные продукты в сторонние системы
  1. Own Your Data – данные могут полностью хранится на стороне пользователей и могут быть абсолютно зашифрованы, что может стать критическим преимуществом на фоне ваших конкурентов
  1. 99.9999% uptime – приложение или существует на устройствах пользователя и крупных клаудах типа Google Drive, или же подключено к вашему S3 серверу, а значит система имеет наименьшую возможную вероятность падения
  1. Максимально интерактивный UI — пользователь никогда не ждет “ответа сервера”, всё меняется локально, поэтому приложение ощущается максимально быстрым, а как только приходят внешние изменения, они моментально отображаются в UI
  1. Приложение всегда будет у пользователя – пока у него есть копия вашего html / css / js приложение всегда будет у пользователя и будет работать, поэтому вы можете продавать свой продукт с гарантией, что человек сможет им пользоваться с этого момента и до самого конца
 

b. Технологически

 
  1. Нет vendor lock-in — нет вообще никакой зависимости от серверных провайдеров
  1. Infinite scalability by design — каждый новый пользователь приносит свои ресурсы, нагрузка не растёт
  1. Скорость разработки – никаких стейджинг и дев серверов, никакого деплоя 10-ка приложений, никакого kubernetes и даже docker, вам не нужно абсолютно ничего, кроме как разрабатывать клиентское веб или нативное приложение, которому просто нужен CDN и все.
  1. E2E тестирование из коробки – вся система это буквально только клиент, поэтому написание теста на клиент это все равно что написать E2E тест, тестирующий всю инфраструктуру
  1. LLM-optimized – все приложение это буквально клиентский код, а значит LLM нужен только контекст этого приложения для знания о всей системе.
  1. Стабильность работы – как часто технология не работало просто по факту отвала backend приложения, или сервера, или БД? В данной ситуации мы имеем offline-first
  1. Offline-first из коробки — приложение работает без интернета, данные всегда под рукой
  1. Non-internet connection – вы можете коммуницировать между устройствами на любом протоколе, например, LAN, BLE, I2C и подобные.
  1. Возможность централизации – в реальности, если в вашем проекте требуется центральная “БД”, в которой будут хранится все или частично данные, вы можете подрубать свое центральное S3-like / GoogleDrive-like хранилище и публиковать часть контента пользователей туда
  1. Real-time-native – как только любое изменение доходит до клиента интерфейс моментально отоюражает
 

c. Безопасность

 
  1. Меньше attack surface — нет сервера = нет точки атаки для хакеров.
  1. Privacy by default — данные не покидают устройство пользователя, не нужно думать о GDPR и утечках.
  1. Теоретически невзламываемая система – возможны стратегии шифрования, при которых ключи создаются и всегда находятся на устройствах пользователей, их буквально не существует ни в одном централизованном месте, и даже если скачать из file storage их невозможно будет расшифровать в обратную сторону.
 

iii. Реализация

Для реализации clientful приложения нам потребуются следующие составляющие:
 
  1. Алгоритм хранения данных
  1. Хранилище
  1. Протокол коммуникации
  1. Auth / Identity
  1. Шифрование
 

a. Алгоритм хранения данных

Нам нужен способ хранить данные на стороне клиентов с учетом возможности:
 
  1. Использования множества устройств – пользователь, используя приложение на другом устройстве (например, на компьютере и телефоне), должен иметь возможность работать над теми же самыми данными
  1. Коллаборации множества пользователей – пользователи должны иметь возможность коллаборативно работать над одними и теми же данными, как по отдельности и видеть сохраненные изменения, так и в real-time
 
И тут важно сделать уточнение, у нас есть 2 модели решения этого вопроса:
 
  1. Pure P2P – каждый узел равнозначен и может полностью самостоятельно принимать решение
  1. Host-base P2P – в какой-то конкретный момент времени или на определенные операции какой-то из узлов должен становится главным и обрабатывать запросы от других узлов
 
Существует несколько алгоритмов, которые реализуют подобные модели:
 
  1. Pure P2P
    1. CRDT — структур данных и алгоритмы, позволяющие нескольким узлам независимо менять одно и то же состояние, а затем без конфликтов смержить изменения так, чтобы все реплики сошлись к одному результату.
    2. CouchDB — eventual consistency документная БД и протокол репликации: узлы обмениваются версионированными документами, а конфликты фиксируются как альтернативные версии и требуют стратегии разрешения (то есть merge не “магический”, как у CRDT, а задаётся моделью/логикой приложения).
    3. Blockchain — распределённый журнал (ledger), где узлы приходят к единому порядку транзакций через механизм консенсуса (PoW/PoS и т.п.). Это “демократия” в том смысле, что нет доверенного центра, но “демократия” достигается ценой дорогого консенсуса.
    4. Git-like log – пользователи буквально или руками, или автоматически по логике приложения создают на изменения ветки, а потом, при обмене сообщениями мерджат друг в друга.
  1. Host-base P2P
    1. OT — подход к коллаборативному редактированию, где изменения представляются как операции (insert/delete/replace и т.д.), а при конкуренции операции трансформируются относительно друг друга так, чтобы все пришли к одному результату.
    2. PAXOS / Raft — алгоритмы консенсуса для репликации лога в кластере: группа узлов выбирает лидера, лидер принимает команды (записи в лог), реплицирует их на кворум.
 
Библиотеки, реализующие этот функционал:
 
  1. Yjs — одна из самых распространенных библиотек CRDT
  1. Automerge – еще одна реализация CRDT, но хранит данные как неизменяемый граф (как git дерево)
  1. OrbitDB – бд основанная на “жернале операци” поверх CRDT, с встроенным протоколом коммуникации libp2p поверх IPFS и расширенными операциями над CRDT структурами
  1. PouchDB – реализации CouchDB протокола на клиенте
  1. Gun.js — распределенная графовая БД с P2P синхронизацией
  1. Hypercore – распределенный append-only иммутабельный лог (и куча экосистемы вокруг)
  1. SQLite + CRDT – библиотеки, которые объединяют CRDT и sqlite
    1. CR-Sqlite
    2. Sqlite-sync
 

Но в реальности смысл имеет только CRDT

Да, сверху я перечислил несколько алгоритмов, но если быть по-настоящему честным, то только CRDT, который реально справляется со всеми условиями, нужными в clientful:
 
  1. OT – чаще всего, все-таки предполагает единый узел, который будет собирать последовательность операций.
  1. CouchDB – протокол разрабатывался с идейей eventual consistency DB, в которой клиенты могут поключаться к центральным узлам и трансформировать часть данных. И на данный момент, попытки сделать его полностью P2P правда существуют, но они абсолютно эксперементальны.
  1. PAXOS / Raft – вот они вполне себе возможны, но они имеет преимущество над CRDT, когда (а) вам нужна строгая транзакционность, (б) вы можете доверять одному из участников. Сложность реализации настолько большая, что смысл прибегать в PAXOS / RAFT только если нет никаких других вариантов решения вопроса.
  1. Blockchain – практически схож с PAXOS / Raft, но требует доверия не к 1 узлу, а большинству узлов, но точно также имеет такую большую технологическую сложность, что его использование должно иметь невероятно весомую причину.
  1. Git-like log – интересная модель, НО предполагает или разруливание со стороны пользователя, или просто не подходит для большого количества бизнес-кейсов.
 
Поэтому в сухом остатке, в 90% случаев, смысл будет иметь только CRDT.
 

Бонус

Большинство реализаций CRDT дают нам набор методов для работы с Key-Value хранилищем, но иногда нам нужно что-то большее: проиндексировать данные, сделать агрегаты, сложную выборку и так далее. Тогда мы вполне себе можем хранить и сами CRDT структуру и их дубликаты в других embedded хранилищах, как раз для таких задач:
 
  1. SQLite – даже без встроенного CRDT, вы можете сохранять CRDT контент прямо в sqlite базу, может быть полезно, если вы хотите, например, часть этого контента индексировать.
  1. PGlite – тоже самое, что и SQLite, но только буквально PostgreSQL на WASM (причем, работающий, так еще и с extensions).
  1. DuckDB – аналитическая встраиваемая БД, которая умеет даже в подгрузку файлов по URL (например, напрямую из S3), что делает ее идеальным кандидатом для использования в clientful.
  1. LanceDB – тоже самое, что и DuckDB, но векторная и из коробки предполагающая хранение во внешних файловых хранилищах
 

b. Хранилища

Где и как мы будем хранить наши данные на каждом узле и какой будем использовать File Storage.
 
И тут мы делим на удаленные и локальные
 
  1. Удаленные
    1. Google Drive-like – идеальный кандидат, потому что такие хранилища есть у большинства клиентов, а значит, есть возможность полностью перенести хранение файлов на них
    2. S3-like – любой Minio, rustfs, Cloud S3 идеально подходит для данной задачи
    3. CDN-like – использование любого CDN провайдера для загрузки и скачивания файлов
    4. IPFS / Filecoin — децентрализованное крипто-хранилище
    5. Nostr-like — децентрализованный протокол хранения/обмена данными без сервера.
    6. Torrent – и опять же, почему нет, это буквально протокол для P2P передачи и хранения данных.
    7. Git-like – использовать git репозитории для персистенции и передачи файлов? А почему нет?
  1. Локальные
    1. Browser Local Storage / IndexedDB / OPFS — нативное браузерное хранилище, идеально для client-side first
    2. File System – буквально локальная файловая система
 

c. Communication

Основная задача протокола коммуникации – дать возможность подписаться на “комнату”, по ее уникальному id (чаще всего, UUID), получать и отправлять всем участникам delta / snapshot данных. С учетом всех остальных частей стэка clientful, больше от протокола коммуникации ничего не нужно, потому что аутентификации и синхронизацией данных занимаются другие части системы.
 
Из протоколов:
 
  1. WebRTC – возможность p2p соединения клиентов
  1. WebSocket – постоянное двустороннее соединение через TCP
  1. QUIC – свеженький протокол на UDP, с гарантиями TCP и защищенностью TLS
 
Тема большая и сложная, особенно, когда мы пытаемся сделать p2p соединение клиентов, посоветую вам поизучать код и труды компании Holepunch, там вы найдете очень много чего интересного и полезного по тематике:
 
 

d. Auth / Identity

Как мы аутентифицируем и даже авторизуем клиентов клиентов:
 
  1. Google Drive-like OAuth
      • Используем OAuth провайдера как источник идентичности (Google/Microsoft/и т.д.).
      • Токены нужны не только для login, но и для доступа к File Storage пользователя (Drive/Dropbox/etc.) — то есть авторизация на операции чтения/записи файлов происходит “встроенно” через провайдера.
      • Плюсы: минимальный UX friction, понятная модель доступов, часто уже есть у большинства пользователей.
      • Минусы: зависимость от провайдера, ограничения/квоты API, возможные сложности с shared-папками и доступами между пользователями.
  1. Sign-in with Ethereum (SIWE)
      • Идентичность = владение приватным ключом кошелька; вход = подпись challenge (nonce) сообщением.
      • Авторизация между пользователями может строиться через on-chain / off-chain ACL: например, “кто может читать/писать” определяется списком адресов и ролей.
      • Плюсы: переносимая идентичность, нет центра, удобно для web3/комьюнити продуктов.
      • Минусы: UX/seed-фразы, кошельки/chain-specific, сложнее для массовой аудитории; всё равно часто нужен внешний канал доставки данных (storage/broker).
  1. Local-first keys (device/user keys)
      • Идентичность строится вокруг локально сгенерированных ключей (например, Ed25519/X25519):
        • ключ устройства (device key) + ключ пользователя (user key) +/или ключи рабочих пространств (workspace keys).
      • Публичные ключи распространяются через File Storage/реестр/инвайты; приватные никогда не покидают устройства.
      • Авторизация = подписи и capability-токены (например, “этот ключ может писать в эту папку/документ”).
      • Плюсы: максимально соответствует идее clientful/privacy-by-default, можно сделать E2E-шифрование.
      • Минусы: восстановление доступа (recovery), ротация ключей, онбординг новых устройств, отзыв доступов.
  1. AT Protocol (Bluesky stack)
      • Идентичность = DID (Decentralized Identifier), привязанная к handle/домену; переносимость аккаунта обеспечивается протоколом.
      • Можно использовать AT как “identity + directory + transport” слой, а данные приложения хранить в File Storage, привязывая права к DID.
      • Плюсы: децентрализованная идентичность, встроенные механизмы репликации/федерирования, потенциально удобно для social-like продуктов.
      • Минусы: стек относительно специфичен; придётся аккуратно разделять, что живёт в AT, а что — в вашем файловом хранилище/CRDT-данных.
 

e. (coming soon) Шифрование

(пока отложу написание этой главы, потому что тема очень большая)
 

f. Backend-like

Несомненно, время от времени получается ситуация при которой нам нужен какой-нибудь вариант backend, например, мы хотим сделать кроулера, который будет собирать информацию с сайтов и складывать ее в наше File DB.
 
Несомненно, именно для этой задачи, можно сделать небольшой backend, например, serverless, который будет работать с нашей File DB. Но эта работа посвящена изучению вариантов, где backend с нашей стороны быть не должно.
 
Какие тогда варианты? Сделать старый добрый self-hosted.
 
Мы можем вшить подобного рода функционал напрямую в приложение пользователя и запускать эти процессы на заднем плане.
 
“Но это же будет backend” – не в традиционном смысле, ведь подобное приложение может просто жить на компьютере пользователя и работать на заднем плане, или же он может разместить его на свой домашний сервер, как сейчас делают с популярным OpenClaw.
 
Да, неидеально, но соответствует главным принципам clientful: вычислительные мощности и хранилище находятся на стороне клиента.
 

iv. Сложности

Что надо учитывать при использовании clientful от более серьезных аспектов к менее:
 

1. Зависимость от реализации CRDT

Во-первых, нет одного верного подхода в реализации CRDT: существуют варианты на самых простых структурах, а существуют Merkel Tree-based CRDT, которые хранят всю историю изменений со встроенной системой отката в прошлое.
 
Соответственно, если у вас на клиентах разные языки программирования, то вам надо надеятся, что будет библиотека, которая есть сразу на всех них
 
Такие библиотеки как Yjs имеют реализации на огромном кол-ве языков + Rust версию, которую по FFI можно самому подключить к любому другому языку, поэтому базово, проблема решается, но не без особенностей.
 

2. Только коллаборация

А если по-другому: clientful не подходит для систем, где надо разруливать / предотвращать конкуренцию.
 
Например: кошельки с деньгами и возможность их отправлять друг другу – каждая операция над доступными клиенту данными считается всеми другими клиентами валидной, а значит можно сделать любую транзакцию с любого кошелька на другой
 
Единственный вариант – вшивать проверку надежности операций в саму систему. Например, использовать подписи приватными ключами и проверку публичными на каждую операцию:
 
  • Использовать подписи приватными ключами и проверку публичными на каждую операцию и в случае некорректности помечать как ненадежные
  • Использовать Merkel Tree для пересчета состояний сущностей с учетом подписей
  • Использовать blockhain-like структур
  • Или назначать 1 master-клиента, который может принимать решения об легальности того или иного действия
 
Но, во-первых, объем требуемого кода несравненно больше любой централизованной системы, во-вторых, легко пропустить edge-кейсы, поэтому в случае конкурентности, лучше оставлять ее централизованной.
 

3. Неконсистентные состояния

Наиболее частая проблема:
 
  1. Группа людей работает над каким-то документом, один из них садится в самолет
  1. Пока он в оффлайне, он удаляет этот документ.
  1. Остальные продолжают работать с тем документом.
  1. Когда он попадает в онлайн, его состояние синхронизируется и так как никто не “отменял удаление”, этот документ реально “удалится”, даже если его коллеги работали над ним
 
И чтобы исправлять такие вещи, придется придумывать особенности бизнес-логики приложения, например:
 
  • При “удалении” сначала перемещать документ в “корзину”, а не удалять, и при выходе в онлайн, если надо документом была проведена работа, то восстанавливать его автоматически
  • Или запрещать удалять документы в оффлайне
  • Или формировать задачу “удаление”, но не применять ее, пока пользователь не вышел в онлайн и не увидел изменения коллег
 
Варианты решения есть, но нужно всегда помнить про эти особенности CRDT.
 

4. “Если Х то можно / нельзя Y” не работает

Даже если вы написали код, который проверят, что клиент “не может сделать Х, если нет / есть Y” (создать комментарий, если нет статьи), то так работать не будет:
 
  1. Клиент А создает пост
  1. Клиент Б получает этот пост
  1. Клиент А создает к нему комментарий
  1. Клиент Б видит комментарий
  1. Клиент А удаляет пост и вместе с ним все (один) комментарий
  1. Клиент А пытается создать к посту новый комментрий, но его запрещают это сделать, потому что поста уже нет
  1. Клиент Б еще не получил информацию про удаленный пост, поэтому спокойно создает второй комментарий
  1. Клиент А и Б получают изменения друг друга и получается, что пост удален, а при этом второй комментарий будет существовать
 
Поэтому логика “if X, than forbid Y” практически не работает, поскольку у вас нет центральной точки синхронизации, которая это правило учтет в любом случае.
 
Тут есть 3 вариант:
 
  1. Aftermath Compensation – после обновления состояния смотреть есть ли комментарии без постов и удалять их
  1. Preemptive Compensation – создавая комментарий (в локальной транзакции) “пересоздать пост, если вдруг он не существует”, то есть вы отправляете всем клиентам и свой комментарий, и пост, к которому вы его создали, а значит, даже если он был удален у клиента А, он будет заново у него создан.
  1. Conflict management – видеть этот конфликт и выводить сообщение пользователям о нем (причем, например, не удалять пост полностью пока он не синхронизируется у большинства узлов)
 
То есть, варианты решения есть, просто это слом стандартной парадигмы людей, которые привыкли к консистентным (CP) приложениям и это требует перестройки подхода к проектированию подобной логики.
 
Если интересно в целом какие это создает проблемы и как они решаются, почитайте что-то про Eventual Consistency.
 

5. Классические проблемы оффлайн приложений

По факту, clientful приложение – это оффлайн приложение, которое умеет синхронизироваться с другими, когда выходит в онлайн, а значит ему свойствены и все проблемы оффлайн приложений:
 
  • Миграции данных – вы должны помнить, что данные у клиента уже лежат и нельзя просто взять и “удалить ключ и поменять его на новый”, вы должны будете в какой-то момент времени мигрировать данные из старого формата в новый
  • Если состояние ломается, вы не можете зайти в центральную БД и посмотреть что произошло, вам придется иметь механизмы удаленной проверки того, что случилось с состоянием у клиента
  • Разные версии на разных устройствах
 
А еще, вы сверху получаете еще одну интересную сложность offline-first приложений (способные работать абсолютно автономно): у разных клиентов и даже на разных устройствах одного клиента могут быть разные версии вашего приложения (например, вы поставляете нативные приложения или кэшируете приложение без форсирования обновлений).
 
Это значит, что над одними и теми же данными в хранилище единомоментно будут работать старые и новые клиенты.
 
Да, эта проблема возможна в любых приложениях, но в случае, когда у вас нет центральной БД и центрального backend, которые могут отвергнуть изменения старых версий, или просто создавать ручки v2, или вы можете потом ручками пересобрать развалившиеся данные, а еще можете протестировать заранее подобную миграцию.
 
В случае с local-first приложениями сложность возрастает в несколько раз.
 

v. Что мы имеем в сухом остатке

clientful – очень непривычная для стандартного web 2.0 парадигма, но и полностью в web3.0 она не вписывается. Есть много ограничений и специфик, к которыми надо привыкнуть. Но именно из-за того, насколько она “другая”, у нее появляются и свои уникальные преимущества, которые просто недоступны стандартным web 2.0 системам.
 
Стал бы я разрабатывать все на clientful? Нет. Но, например, разрабатывать SaaS приложение в стиле Notion, Slack, Miro, Mood-tracker, я рассмотрю clientful во имя скорости разработки, встроенной коллаборации и offline-first, отсутствия стоимости инфраструктуры и own your data, что может дать продуктовые преимущества.
 
clientful – это один из N стульев, и попробовать его важно, чтобы у вас всегда было больше 1-го стула в каждой ситуации.
 

vi. Что нас ждет дальше

Я планирую продолжать дописывать эту работу, раскрывать и добавлять разделы, пока она не превратиться в полноценную книгу.
 
Впереди нас как минимум ждут:
 
  1. Буду рассказывать и показывать как строится библиотеки для clientful
  1. Реальные примеры проблем clientful приложений и используемых технологий и их решения
  1. Разборы существующих проектов
  1. Изучать вопрос транзакционности в подобных системах
  1. Как clientful встраивается в текущую экосистему AI пайплайнов и агентов
  1. Структуры, которые расширяют возможности CRDT (Merkel Tree, Merge Tree, etc.)
  1. Авторизация операций
 
💡
Подписывайтесь на телеграм канал 🦾 IT-Качалка Давида Шекунца 💪, где будут выходить анонсы новых глав и обновления существующих. Спасибо за внимание и мощной вам прокачки 💪
 

vii. Полезные ссылки

a. Технологии формата clientful

 
  1. GitHubGitHubGitHub - alexanderop/awesome-local-first: Useful Links for Everything related to LocalFirst
  1. www.localfirst.fm
  1. Pears_p2pPears_p2pPears | Unleash the Power of P2P
 

b. Транзакции в P2P системах

 
  1. YouTubeYouTubeCRDT Research Meetup // AntidoteDB - Nuno Preguiça
  1. YouTubeYouTubeAnnette Bieniusa - AntidoteDB: highly available, transactional database - Code BEAM Lite Munich 2018
 

c. CRDT

 
  1. ElectricSQLElectricSQLIntroducing Rich-CRDTs | ElectricSQL
  1. EDB Postgres Distributed (PGD) v4.3.8 - Conflicts