こんにちは、lkjsxcです。
ふと思い立って、タイル型ウィンドウマネージャーっぽく使えるNostrクライアントを作り始めました。
lkjstr
lkjstr は、Webブラウザ上で動くNostrクライアントです。
ブラウザ完結、タイル型UI、実行時情報による最適化あたりをコンセプトにしています。
Home、検索、投稿詳細、ユーザープロフィール、設定、統計情報を、ひとつの画面に複数並べて使う想定です。vscodeやzedで必要なタイルを並べて作業するような感覚。

Nostr
このアプリは、Nostrプロトコルに寄り添って作られています。
Nostrとは、中央サーバーに依存しないSNS・メッセージング用の通信プロトコルです。
ユーザーは公開鍵と秘密鍵のペアを持ちます。公開鍵はアカウントのIDに近く、秘密鍵は投稿などへの署名に使います。投稿、プロフィール、リアクションなどはイベントとして扱われ、クライアントはそれを作って署名してリレーへ送り、リレーはリクエストに応じて返します。
特定のサービスにアカウントや投稿が閉じるわけではないので、あるクライアントで作ったイベントを別のクライアントで読んだり、いくつかリレーが使えなくなったとしても、他のリレーからイベントを取ってくることができます。
個々のリレーはイベントを受け取らなかったり消したりもできるので、「必ず消されない」「必ず全員に届く」とは言い切れません。それでも、クライアントを選べること、リレーを選べること、公開鍵で同一性を保てること、複数の場所にイベントを複製できることなど、たくさんのメリットがあります。
分散していると
Nostrクライアントは基本的に複数のリレーと通信します。
リレーは全イベントを持っているわけではなく、レスポンスの速さもバラバラです。あるリレーからはすぐ返ってきて、別のリレーからは遅れてくる。しかも遅いリレーから、他では取れなかったイベントが届くこともありがちです。
検索も同じで、NIP-50対応のリレーならどこでも同じ結果が出るわけではなく、リレーによって返ってくるものが全然違います。新しい投稿を取りたいのに古いものばかり返ってくることもある。
複数リレーから同じイベントが届くので重複排除が必要で、遅いリレーを待つかどうかの判断もあります(待てば取りこぼしは減るが体験が落ちる)。このあたりがNostrクライアントを作る上での面倒なところになります。
最適化
lkjstr では、最初のイベントが返るまでの時間、EOSEまでの時間、イベント数、タイムアウト、エラー、検索結果などを記録して、リクエストの内容やタイミング、遅いリレーの扱いを動的に調節しています。
まだ完璧ではありませんが、何も工夫しなければ多くのリソースを無駄に消費してしまうので、ここは手を抜けません。
キャッシュ
Nostrクライアントにおいて、キャッシュは非常に重要な要素です。
複数タイル設計では、同じイベントに何度もアクセスしがちであるため、lkjstr ではローカルで永続化しています。
しかしながら、中途半端にキャッシュを扱うと、本来リレーに問い合わせて取得すべき投稿を逃してしまうこともあります。
どのリレーを、どの条件で、どの時間範囲まで読んだか——その記録が適切に管理されていないと、リレーへの問い合わせを減らしていいかどうか判断できません。イベントのキャッシュそのものと「確実に読み込めた範囲」を両方扱う必要があります。
署名まわり
署名は、NIP-07ブラウザ拡張に対応しています。
NIP-07はWebアプリがブラウザ拡張を通じて公開鍵取得やイベント署名を行う仕組みで、Webアプリ側が秘密鍵を直接持たずに済むのがポイントです。
パスキーの対応も考えてはいますが、まだ実現していません。
開発とAI
このプロジェクトの開発は、そのほとんどをAIに委ねています。
ドキュメントに重きを置いており、なるべく先に文書を書き、判断や設計の理由を残し、AIが読み返せる形を維持しています。
2026年5月30日時点で、docsは177本・約9,117行、srcは567本・約38,736行、testsは約20,920行。初回コミットは2026年5月17日で、この時点のコミット数は373。srcに200行を超えるファイルはなく、docsに300行を超えるMarkdownもありません。

おわりに
好奇心駆動のふらふら開発ではありますが、より良いアプリを目指して頑張りたいなーと考えています。