Nostr — бэкенд «из коробки»: где подходит, а где нет

от автора

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

Введение

Долгое время я хотел создать простое кроссплатформенное приложение для ведения заметок. Наиболее важным usecase’ом для меня было хранение чувствительной информации, такой как пароли, адреса криптокошельков и т.п. Также ключевым требованием была надёжная синхронизация между девайсами, в частности iOS, Android, macOS.

Но основной проблемой для меня было то, что я долгое время не мог найти секьюрного и независимого от какого-либо конкретного провайдера способа синхронизации заметок.

И вот, какое-то время назад, судьба свела меня с проектом, построенным вокруг протокола Nostr, — так я с ним и познакомился. И хотя для задач того проекта Nostr подходил, мягко говоря, не идеально (почему — станет ясно из статьи), именно для моей задачи с заметками он оказался практически идеальным решением.

Целью данной статьи является обзор положительных и отрицательных кейсов практического использования Nostr-протокола, с которыми лично мне приходилось столкнуться. Также хочу описать задачи, для которых его стоит использовать, и те, для которых следует поискать какие-либо другие решения.

Что же такое протокол Nostr и как он реализован

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

Протокол Nostr — это набор правил взаимодействия (NIP) с децентрализованной сетью серверов (relay). Взаимодействие с ними происходит по WebSockets. Каждый сервер содержит базу данных событий. Событие — это запись с фиксированной структурой, которой клиент и реле обмениваются в виде JSON-объекта по определённой схеме. Пользователь может получать и публиковать такие события с одного или нескольких реле (и на одно или несколько реле). Каждое такое событие имеет id — хеш самого события (точнее, SHA-256 от сериализованного набора полей: pubkey, created_at, kind, tags и content), а не только его контента. Таким образом, при получении одного и того же события с разных реле id используется для дедупликации. Кроме того, каждое событие имеет подпись. Для генерации подписи используется id события и приватный ключ отправителя, а в json-событии имеется поле с публичным ключом отправителя. Таким образом, для того чтобы опубликовать событие на одном или более реле, требуется иметь эту пару ключей. (В то время как для получения событий наличие таких ключей не обязательно).

Легко заметить, что факт наличия у пользователя пары ключей может быть использован для алгоритмов асимметричного шифрования. В спецификации описаны общие подходы к шифрованию сообщений чата — например, актуальный сегодня Encrypted Payloads (NIP-44), на котором строится личка Private Direct Messages (NIP-17). Есть и более старый Encrypted Direct Message (NIP-04): формально он помечен как deprecated и не рекомендуется для публичного децентрализованного использования (неаутентифицированное шифрование, утечка метаданных).

При этом важно понимать, что NIP-44 — не единственный возможный вариант. Протокол не навязывает конкретный алгоритм: клиенты вольны договориться о любой совместимой схеме шифрования контента. Поэтому для прикладных задач — скажем, если вы разворачиваете собственное приватное реле или решаете конкретную бизнес-задачу — вполне может оказаться уместным и NIP-04 с привычным AES, и вообще любая устраивающая вас схема.

Модель данных: события, kind и теги

Если вы уже знакомы с Nostr (события, kind, теги, модель подписок REQ) — смело пропускайте этот раздел и переходите сразу к разбору кейсов.

Прежде чем переходить к конкретным кейсам, имеет смысл чуть подробнее разобрать, как устроено само событие — дальше мы будем постоянно к нему возвращаться.

Любое событие Nostr — это JSON примерно такого вида:

{  "id": "1897f99...",  "pubkey": "9c2e0a7...",  "created_at": 1781787656,  "kind": 1,  "tags": [    ["e", "737f50..."],    ["p", "9c2e0a7..."]  ],  "content": "Привет, Nostr!",  "sig": "41b737f50..."}

Поле kind — это целое число, которое задаёт скорее приблизительный смысл события, чем строгий тип. Например, kind:0 — профиль пользователя, kind:1 — короткая текстовая заметка (пост), kind:7 — реакция (лайк). Важно, что это договорённость, а не жёсткая схема: значение каждого kind описано в соответствующем NIP, а большинство NIP-ов имеют статус draft — то есть это конвенции, а не зафиксированный стандарт. Реле не валидирует структуру содержимого: за интерпретацию kind и формата события целиком отвечают договорившиеся между собой клиенты. Можно сказать, kind — это что-то вроде «таблицы» в привычной БД, но без жёсткой схемы и серверной валидации.

Кроме смысла, kind задаёт ещё и семантику хранения на реле. Конкретное поведение зависит от диапазона, в который попадает число (диапазоны заданы в NIP-01). Если упрощённо, события делятся на:

  • обычные (regular) — диапазон 1000–9999 (а также kind:1, kind:444); просто накапливаются и ничего не перетирают;

  • заменяемые (replaceable) — диапазон 10000–19999 (а также kind:0 — профиль, kind:3 — список контактов); реле хранит только самую свежую версию для данного автора, новое событие вытесняет старое;

  • адресуемые (addressable, они же параметризованные заменяемые) — диапазон 30000–39999; то же, что заменяемые, но в «координаты» замены входит ещё и значение тега d (см. ниже). Именно так работают обновляемые сущности с идентификатором — длинная заметка (kind:30023), определение сообщества (kind:34550);

  • эфемерные (ephemeral) — диапазон 20000–29999; реле не обязано хранить их вообще (полезно для сигналинга).

Для addressable-событий «координатами» служит тройка kind:pubkey:d, где d — значение специального тега ["d", "..."], стабильный идентификатор сущности. Именно на такие события ссылаются через тег a в формате kind:pubkey:d (увидим ниже).

Поле tags — это массив массивов: первый элемент каждого вложенного массива — имя тега, дальше идут его значения (["e", "<id события>"], ["p", "<pubkey>"] и т.д.). Теги с однобуквенными именами (e, p, a, g, d, …) реле индексирует, и по ним можно фильтровать запросы — в фильтре это записывается как #e, #p, #g (это пригодится в разделе про ленту).

Наконец, о том, как события запрашиваются. Чтобы получить события, клиент открывает подписку сообщением вида:

["REQ", "<идентификатор подписки>", { <фильтр> }, { <фильтр> }, ...]

Фильтр — это JSON-объект с полями вроде authors, kinds, since, until, limit, #e, #g. Реле возвращает все события, подходящие хотя бы под один фильтр, и держит подписку открытой — досылая новые подходящие события по мере их появления, пока клиент не закроет её сообщением CLOSE. К структуре фильтров и их ограничениям мы ещё вернёмся.

Заметки

В ходе реализации своего приложения я особо не задумывался о том, как расшифровывается аббревиатура Nostr. Nostr — это акроним: Notes and Other Stuff Transmitted by Relays («заметки и прочие штуки, передаваемые через релеи»)

Таким образом, идея создания приложения-заметок на основе этого протокола лежала на поверхности. Nostr предоставляет возможность синхронизации и даёт рекомендации по сильному асимметричному шифрованию — Encrypted Payloads (NIP-44).

Таким образом, Nostr очень хорошо подходит для этой задачи.

Собственно, именно так и появился мой пет-проект — Private Notes (Nostr): кроссплатформенное приложение для заметок с end-to-end шифрованием на устройстве (NIP-44), подписью через secp256k1 и хранением на выбранных пользователем реле. Ни сервера, ни аккаунтов — только пара ключей. Дальше по тексту я буду опираться на него как на пример, а детали реализации можно посмотреть прямо в исходниках — github.com/AlexeyYuPopkov/nostr_notes (репозиторий открытый; если заметите баг или есть идея — буду рад Issue).

Чаты

Во многих небольших мобильных приложениях есть функция чата. Для большинства таких проектов в качестве бэкенда для чата используются сторонние (third-party) решения, такие как Twilio, Quickblox, или же облачные базы данных, например Firebase Realtime Database, Cloud Firestore. Однако это требует дополнительных затрат на подписку на эти сервисы. Во втором случае также необходимо проектирование БД для данных, связанных с чатом, а это — дополнительные трудозатраты и баги. Nostr же даёт готовый бэкенд, который хорошо подходит для чатов. При этом можно либо использовать множество публичных, уже работающих реле, либо развернуть своё, на своём бэкенде, скачав популярную реализацию с GitHub.

Отдельно стоит упомянуть проблему медиаконтента в сообщениях чата. Nostr-событие — это всегда JSON с фиксированной схемой, и хранить бинарные данные (изображения, файлы) непосредственно в событии протокол не предполагает. Для решения этой задачи существует NIP-96 — стандарт, описывающий HTTP File Storage: спецификация для файловых хостингов, совместимых с Nostr. Клиент загружает файл на NIP-96-совместимый сервер, получает URL и включает его в контент события. Публичные NIP-96 серверы существуют, и в целом это работает.

Однако на практике, если проект уже использует централизованную инфраструктуру (AWS S3, Firebase Cloud Storage и т.п.), проще и надёжнее хранить медиа там, а в событии Nostr просто размещать ссылку. Подход прозаичный, но для большинства продуктов — вполне оправданный.

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

Я бы отметил две такие проблемы

  1. производительность шифрования сообщений

  2. модерация групп

Суть первой из них в том, что для применения асимметричного шифрования (Encrypted Direct Message / NIP-04 или Encrypted Payloads / NIP-44) возникает необходимость шифровать сообщение для каждого пользователя в отдельности. Такой подход при возрастании количества участников группы может отрицательно сказаться как на производительности, так и на общей надёжности. Хотя, по своему опыту скажу, что для групп с количеством участников <= 20 человек существенных проблем с производительностью я не наблюдал. Читал, что и до 100 человек проблем быть не должно. И в то же время для обеспечения избыточной надёжности я бы предпочёл использовать для шифрования контента сообщений централизованные решения, вне протокола Nostr (хотя это и менее секьюрно).

Вторая проблема — это модерация группы и добавление/удаление участников из неё. Существующее решение Private Direct Messages (NIP-17) — это, по сути, групповая переписка без отдельного управляемого объекта группы. Метаданные группы (название и т.п.) не хранятся как единая сущность, а вычисляются из тегов самих сообщений, причём «выигрывают» теги самого свежего сообщения. Как следствие, нельзя «жёстко» добавлять или удалять участников, а переопределить метаданные группы фактически может любой участник — просто отправив новое сообщение с нужными тегами. Корень проблемы — в основе самого протокола Nostr: изменять любое событие может только его создатель. Таким образом, если создать событие — модель комнаты чата — и связать с ней сообщения путём ссылки на комнату, то модерировать это событие сможет только его создатель. То есть модератор только один. Однако даже такое решение является несколько синтетическим — такого NIP (стандарта в Nostr) я не видел.

Следует отметить и решение этой проблемы — NIP-29. Оно предполагает развёртывание своего, отдельного реле для модерации групп (хотя существуют и публичные relay-29, например wss://relay.groups.nip29.com). Само реле проверяет, имеет ли модератор право изменять состояние группы. Такое реле ответственно только за состояние самой группы, но не за сообщения пользователей, которые хранятся на обычных реле. Однако это, скорее, централизованное решение (иначе теряется единственный источник правды и вероятна рассинхронизация).

В целом relay-29 — это хорошее централизованное решение для групповых чатов с ролями и модерацией, которое действительно работает.

Лента, посты

В моём понимании социальная сеть — это прежде всего посты, лайки, комментарии, репосты.

Давайте попробуем разобраться, насколько хорошо протокол Nostr подходит для этих целей и какие есть ограничения. Мы уже знаем, что реле хранят модели событий и по WebSockets могут возвращать их JSON. Для наглядности приведу пример возможного JSON поста:

    {        "created_at": 1781787656,        "id": "1897f99...",        "kind": 1,        "pubkey": "9c2e0a7...",        "content": "Some text...",        "sig": "41b737f50...",        "tags": [["e", "737f50..."],["e", "1897f..."],...,["a", "30023:f72...:abcd"],["a", "30023:er34...:abcd"],...,["g", "u4pr"],["g", "u4pru"]]    }

Все теги — опциональны. Пройдёмся по тем, что в примере:

  • e — ссылка на другое событие по его id; используется для организации тредов, цитирований и ответов

  • a — ссылка на адресуемое (replaceable) событие в формате kind:pubkey:d; в примере выше — ссылки на события kind:30023 (длинные посты / статьи по NIP-23) разных авторов

  • ggeohash локации, с которой связан пост; чем длиннее строка, тем точнее координата (u4pr — грубый регион, u4pru — чуть точнее)

Теперь, чтобы понять ограничения nostr, рассмотрим несколько кейсов:

  1. Чтобы отобразить в ленте, например, посты (обычно kind:1) конкретных авторов (см. authors), нужно отправить запрос

["REQ", "feed-sub-1", {  "authors": ["9c2e0a7...", "pubkey1", "pubkey2", ...],  "kinds": [1],  "until": 1718700000,  "limit": 20}]

Пока всё OK. При помощи until + limit тут легко организовать пагинацию по времени, которая будет надёжно работать.

  1. Теперь усложним задачу. Допустим, нам нужно построить ленту таким образом, чтобы показывать посты конкретных авторов ИЛИ посты пользователей с близкой локацией:

["REQ", "feed-sub-2", {  "authors": ["9c2e0a7...", "pubkey1", "pubkey2", ...],  "kinds": [1],  "until": 1718700000,  "limit": 20},{  "kinds": [1],  "#g": ["u4pr"],  "until": 1718700000,  "limit": 20}]

Здесь сразу возникает проблема. Внутри одного объекта-фильтра все условия работают по И (AND) — то есть authors + kinds + until должны выполняться одновременно для одного события. А вот чтобы получить ИЛИ между «постами нужных авторов» и «постами с нужным geohash», нужно использовать два отдельных фильтра в массиве — реле вернёт объединение (union) результатов обоих. И именно здесь ломается надёжная пагинация по until+limit. Каждый фильтр в массиве лимитируется независимо: в типичной реализации реле применит limit: 20 к каждому из двух фильтров отдельно (точное поведение при объединении результатов нескольких фильтров зависит от реализации релея), а затем смешает результаты. Это значит, что если в первом фильтре (по авторам) накопилось 20 более новых постов, а во втором (по geohash) — тоже 20, но более старых, итоговая выдача после сортировки по времени и обрезки до общего лимита на клиенте может «съесть» только часть одного из источников, пропустив часть другого. При следующем запросе с новым until, рассчитанным по самому старому полученному посту, для одного из фильтров until окажется некорректным — он либо пересечёт уже показанные посты повторно, либо пропустит часть так и не дошедших до клиента событий. Получаются дыры в ленте, которые накопительно усугубляются при каждой следующей подгрузке.

Рабочий, но более трудоёмкий способ — пагинация через динамически расширяющееся временное окно (since + until), которое раздвигается, если в него попало мало событий. У такого подхода свои недостатки: непредсказуемое количество запросов к релею для одной страницы ленты, сложность отображения индикатора загрузки и риск долгого «зависания» подгрузки в периоды низкой активности.

Третья проблема — на уровне самого geohash-фильтра: совпадение тега g работает по точному равенству строки, а не по префиксу. Если в событии указан ["g", "u4pr"] (грубая локация), а фильтр запрашивает "#g": ["u4pruyd"] (более точную), реле это событие не вернёт — хотя u4pruyd физически лежит внутри u4pr. Обходят это, публикуя на событие сразу несколько geohash-тегов разной длины (все префиксы), но тогда клиенту, делающему запрос, приходится перечислять в фильтре все префиксы нужной зоны — это плохо масштабируется и раздувает фильтр при попытке покрыть широкую область.

В модели события выше можно заметить теги a и e. С их помощью можно связать пост с каким-либо другим событием (например, с другим постом, если пост представляет собой комментарий к нему, — обычно через e-тег, или комментарий к какому-либо изменяемому событию — через тег a). Детальнее теги e и a описаны в NIP-01. Также пост может иметь лайки, которые обычно реализуются через события kind=7, и комментарии (e). Чтобы получить количество лайков и комментариев и отобразить их в ленте, нужно открыть подписки на соответствующие события и затем их пересчитать. Таким образом, после получения поста, узнав его id, нужно отправить запрос:

["REQ", "post-stats-sub", {  "kinds": [7, 1],  "#e": ["1897f99..."]}]

Однако лайки и комментарии — далеко не единственное, на что может ссылаться (или что может ссылаться на) конкретный пост. Через теги e/a на событие могут указывать репосты (kind=6/16), zap-квитанции (kind=9735), реакции произвольным контентом помимо лайков, цитирования в составе других постов и так далее — и для каждого такого типа связи, по сути, нужна своя отдельная подписка с собственным фильтром по kinds, либо более широкий фильтр без ограничения по kinds, который потом придётся фильтровать и разбирать на клиенте. Уже на этом этапе видно, что для полноценного отображения одной карточки поста в ленте может потребоваться не один, а целый набор параллельных запросов к релею. Более того, здесь есть и менее очевидная опасность: поскольку ссылки через e/a ничем не ограничены протокольно, ничто не мешает событиям ссылаться друг на друга циклически — пост A комментирует пост B, который сам помечен как комментарий к посту A (вольно или из-за ошибки клиента, либо намеренно, в качестве спама/атаки). Клиент, наивно обходящий дерево связанных событий (например, чтобы построить тред целиком), рискует попасть в бесконечный цикл обхода, если заранее не предусмотреть защиту — отслеживание уже посещённых id или ограничение глубины рекурсии.

Таким образом, в то время как Nostr хорошо подходит для организации простых лент, для более сложных могут возникнуть проблемы: очень жадная загрузка контента и, вследствие этого, очень большое количество запросов и плохая производительность.

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

Хотя, может, и без умной выдачи лучше: а то как-то не здорово, что найти понравившийся пост повторно часто не получается.

Community

Среди функционала, построенного на основе Nostr, с которым приходилось работать — это NIP-72. Кратко суть NIP-72: это NIP, описывающий модерируемые тематические сообщества (community) поверх обычных событий Nostr. Сообщество определяется адресуемым событием (kind:34550), в котором заданы его модераторы. Любой пользователь может опубликовать пост со ссылкой на это сообщество — то есть предложить публикацию в community. Но чтобы пост реально считался частью сообщества и отображался в его ленте, один из модераторов должен опубликовать отдельное событие-одобрение (kind:4550), которое содержит исходный пост целиком и подписано ключом модератора. Клиенты при сборке ленты сообщества ориентируются именно на эти approval-события, а не на сами посты с тегом — то есть отбор и модерация здесь полностью на стороне публичных, верифицируемых подписей.

В принципе, это работает. Но когда приходит менеджер и говорит, что модерировать community должен мочь любой модератор без необходимости одобрения создателем community, возникает проблема. NIP-72 в принципе не предполагает, что кто-то из списка модераторов сможет сам отредактировать само определение community — список модераторов в kind:34550 это просто перечисленные p-теги внутри события, подписанного его владельцем. Изменить это событие (а значит и сам список модераторов) технически может только тот, кто держит приватный ключ, которым оно было подписано изначально — то есть создатель community, а не кто-либо из перечисленных модераторов. Это прямое следствие базового ограничения Nostr: редактировать или переопределять состояние события может только обладатель ключа, который изначально авторитетен для этого состояния, и протокол никак не дробит эту авторизацию на роли. Попытка решить эту задачу средствами одного лишь Nostr, без выхода за рамки протокола, в итоге ни к чему хорошему не приводит. На практике проблему закрыли централизованной надстройкой — дополнительным сервисом, который добавляет к событиям дополнительные подписи поверх протокольной модели. Но даже с этим решением я остался крайне недоволен и самим функционалом communities, и итоговой реализацией.

Ещё один похожий usecase — организации

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

Реализация подобного функционала средствами одного лишь Nostr — затея сомнительная. У протокола нет встроенной модели ролей, иерархии «организация → суборганизация» или приглашений; всё, что есть — это подпись одним ключом как единственный механизм авторизации. Здесь стоит упомянуть, что у Nostr был и «родной» механизм делегирования подписи — NIP-26 (Delegated Event Signing), но он так и не прижился и сейчас помечен как deprecated. То есть даже протокольный механизм делегирования в итоге признали неудачным, и на практике задачу закрывают централизованно — например, через keycast, сервис для делегирования подписи в Nostr, разработанный специально для командной работы с ключами. Но даже keycast не решает задачу полностью, в частности — проблему работы с ролями: чтобы дать командам контроль над тем, кто и что может подписывать, его авторам пришлось выстраивать вокруг ключей собственную, протоколу не свойственную систему политик и разрешений. Это показательно: даже специализированный инструмент для делегирования подписи в итоге сводится к написанию полноценного бэкенда с нуля. Поэтому для управления ролями и иерархией организаций тоже потребуются централизованные решения — причём не только для делегирования подписи, но и отдельно для управления ролями. Объём такой работы сопоставим с написанием отдельного бэкенда под эту задачу, и использование Nostr в качестве бэкенда или даже его части здесь выглядит неоправданным.

Выводы

Если пройтись по всем этим кейсам — от заметок и one2one-чатов до групп, лент и организаций, — вырисовывается довольно чёткая закономерность.

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

И для ряда задач это решение подходит действительно хорошо — заметки, чаты (особенно one2one), простые ленты постов без претензий на сложную выдачу. Здесь Nostr закрывает классические проблемы (синхронизация, шифрование, identity) практически без необходимости писать свой бэкенд вообще.

Но по ходу работы накопилось и несколько уроков, которые я для себя вынес.

Не нужно пытаться построить на Nostr полноценную социальную сеть уровня Facebook или любой топовой соцсети. Как только в дело вступают умная выдача, гибкая фильтрация, ролевые модели с иерархией и прочий функционал, давно стандартный для топовых соцсетей — протокол начинает трещать по швам. Не потому, что разработчики NIP-ов что-то недодумали, а потому что сама модель (реле как тупое хранилище подписанных событий, без сервера, имеющего полное и единоличное право на сложную бизнес-логику) для этого не предназначена.

Не стоит обходить ограничения протокола. Соблазн «дотянуть» Nostr до нужной бизнес-логики кастомными надстройками — будь то синтетическая модель модератора, дополнительные подписи поверх событий или сторонние сервисы делегирования — велик, и технически почти всегда возможен. Но, как показывают кейсы с NIP-72 и организациями, в какой-то момент эти надстройки начинают весить больше, чем сам протокол, который они должны были упростить. И, что важнее, обход ограничений не проходит бесплатно: помимо потери децентрализации и совместимости с остальной Nostr-экосистемой (другими клиентами, реле, NIP-ами), такие надстройки — это всегда дополнительная плоскость для багов, причём багов в коде, который никто, кроме вас, не тестировал на масштабе.

Перед тем как браться за реализацию — стоит трезво взвесить оба пути: использовать Nostr с надстройками или задуматься об отдельном бэкенде под конкретную задачу. Свой бэкенд для ролей, иерархии и модерации пишется без оглядки на чужие протокольные ограничения вроде «единственного владельца ключа» и часто оказывается быстрее, чем городить обходные пути вокруг Nostr — особенно если задача с самого начала плохо ложится на модель «подписанные события + реле».

В сухом остатке: Nostr — отличный инструмент, когда задача попадает в его «родную» нишу — и удивительно неудобный, когда вы пытаетесь натянуть на него что-то, для чего он не задумывался. Как и с любым инструментом, главное — заранее понимать, в какую из этих двух категорий попадает ваша задача, а не выяснять это postfactum, после месяца разработки.

Приложение из статьи

Private Notes (Nostr) написано мной и выложено в открытый доступ (исходный код — на GitHub, ссылка в разделе «Заметки» выше). Сборки: App Store, веб-версия, Android (APK).

ссылка на оригинал статьи https://habr.com/ru/articles/1050348/