はじめに
「リアルタイムに同期するアプリを作りたいけど、バックエンドサーバーとDBの設計・運用が重い」——そう感じたことはないでしょうか。FirebaseのようなBaaSは便利ですが、結局は特定のクラウドベンダーに依存することになります。
Gun(GunDB)は、この悩みに違う角度から答えるライブラリです。サーバーを必須とせず、ブラウザ同士やNode.jsのピアがP2Pで直接データをやり取りし、グラフ構造のデータをリアルタイムに同期します。ローカルではlocalStorageにも自動保存されるため、オフラインでも動作し続けるのが特徴です。
とはいえ、読むより触った方が早いと思います。ブラウザだけで動くデータベースがどんな挙動をするのか、先に見たい方はこちらからどうぞ。
Gunとは
Gunは、分散グラフデータベースを謳うJavaScriptライブラリです。2014年から開発が続けられており、npmパッケージgunは非推奨化されておらず、GitHubリポジトリも2026年時点で活発にコミットが続いています。19,000以上のGitHubスターを集め、Internet Archiveなど大規模サービスでの採用実績も持ちます。
主な特徴
- サーバー不要のP2P同期 -
Gun()を呼ぶだけでインスタンスが作られ、ピア同士が直接データを同期します。中央サーバーを置くこともできますが、必須ではありません - オフラインファースト - ブラウザではlocalStorageに自動で永続化されるため、オフラインでも読み書きができ、再接続時に自動でマージされます
- CRDTによる競合解決 - 複数箇所から同時に書き込んでも、Gunの内部アルゴリズムが競合を自動的に解決します
- SEAによる暗号化・認証 -
SEAモジュールで公開鍵暗号やユーザー認証を組み込めます
インストール
npm install gun
yarn add gun
pnpm add gun
Gunのサンプルを動かす
以下はプロフィール情報をgun.get('profile').put()で保存し、.on()でリアルタイムに変化を検知して画面に反映するサンプルです。名前や年齢の入力欄を書き換えると、保存直後に下の表示が更新されます。
まず要点だけを抜き出すと、Gunの基本操作は次の3つに集約されます。
import Gun from 'gun'
const gun = Gun()
// データを保存(部分更新は既存データにマージされる)
gun.get('profile').put({ name: 'Mark', age: 30 })
// 変化をリアルタイムに購読
gun.get('profile').on((data) => {
console.log('更新を検知:', data)
})
// ネストしたキーだけを更新することもできる
gun.get('profile').get('age').put(31)
実際に動かせるサンプルは以下です。入力欄に文字を打つとgun.get('profile').put()が即座に呼ばれ、.on()のコールバックがその変化を検知して画面下部の表示を書き換えます。
名前欄と年齢欄はそれぞれ別の.put()呼び出しですが、どちらも同じprofileノードにマージされて保存されます。片方だけ書き換えても、もう片方の値が消えないことが確認できるはずです。これがGunの「部分更新は既存データにマージされる」という挙動です。
基本的な使い方
最小構成であれば、Gunのインスタンス生成からデータの読み書きまでは数行で完結します。
import Gun from 'gun'
const gun = Gun()
gun.get('greeting').put({ text: 'こんにちは' })
// once() は購読せず、その時点のデータを1回だけ取得する
gun.get('greeting').once((data) => {
console.log(data.text) // "こんにちは"
})
.on()は変化のたびに呼ばれ続ける購読、.once()はその瞬間の値を1回だけ取得します。UIに常時反映したい値は.on()、初期表示だけ欲しい値は.once()と使い分けるのが基本方針です。
実践的なユースケース
リアルタイムなToDoリストを作る
Gunは.set()でコレクションに要素を追加し、.map()で列挙できます。複数のクライアントが同じノードを購読していれば、誰かが追加・更新した項目が他の画面にも即座に反映されます。
const todos = gun.get('todo-list')
// コレクションへの追加
todos.set({ text: '牛乳を買う', done: false })
// 全要素をリアルタイムに列挙
todos.map().on((todo, id) => {
console.log(id, todo.text, todo.done)
})
// 個別の項目はソウル(id)を使って更新
gun.get(id).put({ done: true })
下のサンプルでは、入力欄にテキストを打ってEnterキーを押すとtodos.set()で項目が追加され、todos.map().on()が検知してリストに描画します。項目をクリックするとdoneフラグが反転し、チェック表示が切り替わります。
doneの状態をli.dataset側で管理しているのは、.map().on()のコールバックが新しいデータで再実行された際に、クリックハンドラが古いtodoの値を参照し続けてしまう(クロージャの古い値を掴む)のを避けるためです。複数箇所から同時に更新される可能性があるデータほど、最新値をどこから読むかに気を配る必要があります。
SEAでデータを暗号化する
Gunにはセキュリティ機能をまとめたSEA(Security, Encryption, Authorization)モジュールが同梱されています。SEA.pair()で鍵ペアを生成し、SEA.encrypt() / SEA.decrypt()でデータを暗号化・復号できます。
import SEA from 'gun/sea'
const pair = await SEA.pair()
const encrypted = await SEA.encrypt('内緒のメモ', pair)
console.log(encrypted) // 暗号化された文字列
const decrypted = await SEA.decrypt(encrypted, pair)
console.log(decrypted) // "内緒のメモ"
下のサンプルでは、テキストを入力してボタンを押すとSEA.encrypt()で暗号化した文字列をgun.get('secret-note').put()で保存し、続けてSEA.decrypt()で復号した結果も表示します。保存されている生データが暗号文字列であることが、画面の2行を見比べると分かります。
SEA.encrypt()は鍵ペアごとに異なる結果を返すため、同じ鍵ペアを持つ側でなければSEA.decrypt()で元に戻せません。Gunのデータはノードを知っていれば誰でも読めてしまう仕様なので、他人に見られたくない値はこのように明示的に暗号化してから.put()する必要があります。
投稿とコメントをグラフでつなげる
Gunは名前の通りグラフデータベースなので、あるノードから別のノードを参照する形でリレーションを表現できます。post.get('comments')のように、投稿ノードの中にコメント一覧ノードへの参照を持たせるイメージです。
const post = gun.get('post').put({ title: '最初の投稿' })
const comments = gun.get('post-comments')
post.get('comments').put(comments) // 投稿からコメント一覧への参照を張る
comments.set({ text: 'いいね!' }) // コメントを追加
comments.map().on((comment) => {
console.log('コメント:', comment.text)
})
下のサンプルでは、コメント欄にテキストを入力してEnterを押すとcomments.set()で新しいコメントノードが追加され、comments.map().on()が検知してリストに反映されます。投稿タイトルとコメント一覧が別ノードとして存在し、参照でつながっている点がポイントです。
post.get('comments')をたどるとcommentsノードにアクセスできるため、投稿単位でコメント一覧を切り替えるような設計も同じ考え方で組めます。リレーショナルDBの外部キーに近い感覚で、グラフの参照を使ってデータをつなげられるのがGunらしいところです。
まとめ
Gunは、サーバーを前提にしないという発想でリアルタイムデータ同期を実現するライブラリです。.put() / .get() / .on()という最小限のAPIでグラフ構造のデータを扱え、.set() / .map()でコレクションを、SEAでセキュリティを組み込めます。
小さなプロトタイプやオフライン対応のツールから、P2Pベースのアプリケーションまで、バックエンドの構築コストを下げたい場面で選択肢に入れてみる価値があるライブラリです。まずは今回のサンプルを書き換えながら、ノードの参照や購読の挙動を手で確かめてみてください。