
AT Protocol参照実装のフォークで、パフォーマンス最適化されたAppView、Rustベースのfirehoseインデクサー、Redisキャッシング、そして大規模な自己ホスト型ソーシャルネットワーキングのためのコミュニティ機能を備えています。
これは、Bluesky Social PBC による AT Protocol リファレンス実装 の Blacksky フォークです。api.blacksky.community の AppView を動かしています。
透明性のため、そして他のコミュニティがこの作業の恩恵を受けられるように公開しています。このリポジトリはコントリビューション、Issue、PR を受け付けていません。 正規の atproto 実装が必要な場合は、bluesky-social/atproto を使用してください。
すべての変更は packages/bsky (AppView ロジック)、services/bsky (ランタイム設定)、および1つのカスタムマイグレーションにあります。それ以外はすべて上流です。
上流のデータプレーンには、イベントを直接インデックスする TypeScript の firehose consumer (subscription.ts) が含まれています。いくつかの理由から、これを Rust 製のインデクサ rsky-wintermute に置き換えました。
このリポジトリのデータプレーンと AppView はそのまま動作します。これらは wintermute が書き込む PostgreSQL データベースから読み取ります。組み込みの firehose サブスクリプションを開始しないだけです。
これらは、大規模な AppView をセルフホストするすべての人に広く役立ちます。
LATERAL JOIN クエリ最適化 (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline と getListFeed を PostgreSQL の LATERAL JOIN で書き換え、フルテーブルスキャンではなくユーザーごとのインデックス使用を強制。数千のアカウントをフォローしているユーザーにとって大きな改善。Redis キャッシング層 (packages/bsky/src/data-plane/server/cache/)
Timestamp オブジェクトが .toDate() メソッドを失い、キャッシュヒット時にプロファイルのハイドレーションが不完全になります。現在は Redis キャッシュを無効にして運用しています。修正方法は、キャッシュ書き込み時にタイムスタンプを ISO 文字列としてシリアライズし、読み取り時に再構築することです。通知設定のサーバーサイド強制 (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons を指定しない場合、サーバーはユーザーの保存された通知設定を適用します。これがないと、設定はクライアントサイドでのみ強制され、効果がありません。認証ベリファイアの古い署名鍵修正 (packages/bsky/src/auth-verifier.ts)
forceRefresh)に、データプレーンのインメモリ ID キャッシュをバイパスし、PLC ディレクトリから直接 DID ドキュメントを解決します。アカウント移行後に署名鍵がローテーションされたがキャッシュに古い鍵が残っている場合の認証失敗を修正します。JSON サニタイゼーション (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) と制御文字を除去します。これらは RFC 8259 では有効ですが、Node.js の JSON.parse() では拒否され、データプレーン内で rowToRecord の解析失敗が静かに発生し、投稿が欠落する結果になります。個別の PDS ではなく AppView 上に存在するプライベートコミュニティ投稿のためのインフラストラクチャ。Blacksky の仕組みに固有ですが、他のコミュニティの参考になる可能性があります。
community.blacksky.feed.* と、提出、取得、削除、タイムライン、スレッドビューのエンドポイントcommunity_post テーブル(マイグレーション: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 との統合BLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(Rust indexer) | (Go search)
- firehose consumer | |
- backfiller | v
- label indexer | OpenSearch
- direct indexer |
v
bsky-dataplane (gRPC :2585) <--- Redis (optional)
|
v
bsky-appview (HTTP :2584)
|
v
Reverse proxy (Caddy/nginx)
Wintermute は、4つの並列処理パスを持つモノリシックな Rust サービスです。
bsky.network firehose に接続し、イベントを Fjall (組み込みキーバリューストア) キューに書き込むON CONFLICT を使用して PostgreSQL に書き込むrsky リポジトリに含まれる追加の CLI ツール:
queue_backfill -- CSV、PDS ディスカバリ、または直接の DID リストからバックフィル用の DID をキューに入れるdirect_index -- キューをバイパスして特定のリポジトリを取得・インデックス(個別アカウントの修正に有用)label_sync -- カーソル0からラベルストリームをリプレイし、見逃した否定をキャッチアップplc_import -- PLC ディレクトリからハンドル/DID マッピングを一括インポートpalomar-sync -- フォロワー数と PageRank を OpenSearch に同期PDS が Bluesky の video.bsky.app をサポートしていないユーザー向けのビデオアップロードサービス。独自の DID (did:web:video.blacksky.community) を使用して、サービス認証 JWT 経由でユーザーの PDS に認証します。フロー:
モデレーションラベルは、WebSocket サブスクリプションを介してラベラーサービス(例:Bluesky の Ozone)から届きます。Wintermute の ingester は、専用の label_live キュー(低ボリューム、メインの firehose とは別)でラベルを処理します。label_sync ツールは、ラベラーの全ストリームをリプレイして、ラベルを再挿入せずに見逃した否定(ラベルの削除)をキャッチアップできます。
bsky スキーマを持つ PostgreSQL 17bsky スキーマはデータプレーンのマイグレーションによって作成されます。初回実行時に、データプレーンはすべてのマイグレーションを自動的に適用します。Blacksky 固有のマイグレーションは 20260202T120000000Z-add-community-post.ts(コミュニティ投稿テーブル)のみです。コミュニティ投稿が必要ない場合は、それを削除できます。
rsky-wintermute は同じスキーマに書き込みます。すべての INSERT 文は ON CONFLICT を使用しているため、wintermute とデータプレーンのマイグレーションを任意の順序で実行しても安全です。
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
フルネットワークのバックフィル(全ユーザー約4,200万、記録約185億)は、wintermute の並列処理でも数週間かかります。目安:
バックフィル中、AppView は機能しますが、まだバックフィルされていないユーザーのデータは不完全に表示されます。ライブイベントはバックフィルの進行に関係なく即座にインデックスされます。
これらは、フルネットワークの AppView をブートストラップする際に遭遇した問題です。同じことを行う場合、おそらくこれらのいくつかに直面するでしょう:
COPY テキスト形式の JSON 破損: PostgreSQL の COPY テキストプロトコルは、バックスラッシュをエスケープ文字として扱います。バルクローダーが JSON 文字列内のバックスラッシュをエスケープしない場合、\" は " になり、レコードが黙って破損します。record.json カラムは text 型(jsonb ではない)であるため、PostgreSQL はこれを検出しません。約66,000の破損レコードを発見し、公開 API から再取得して修復する必要がありました。
JSON の null バイト: 一部の AT Protocol レコードには \u0000(null バイト)が含まれており、これは RFC 8259 では有効な JSON ですが、Node.js の JSON.parse() では拒否されます。データプレーンはこれらのレコードに対して黙って null を返します。データベースに書き込む前に null バイトを取り除いてください。
タイムスタンプ形式の感度: データプレーンは、ミリ秒精度と Z サフィックス (2026-01-12T19:45:23.307Z) を持つタイムスタンプを期待します。ナノ秒精度やタイムゾーンオフセット形式 (+00:00) では、ソートや比較に微妙な問題が発生します。
通知テーブルの肥大化: (did, recordUri, reason) に一意制約がないと、通知テーブルは重複で無制限に成長します。当社では、気づく前に13億行(663 GB)に達しました。INSERT に ON CONFLICT DO NOTHING を追加しても、一意インデックスが先に存在する場合にのみ効果があり、インデックスを作成するには既存のデータの重複排除が必要です。
投稿埋め込みテーブル: post_embed_image と post_embed_video テーブルは、インデクサがそれらを処理しない場合、デフォルトでは入力されません。これがないと、getAuthorFeed のメディアフィルターは何も返しません。これらは個別にバックフィルする必要があります。
ラベル否定の順序: ラベルの否定(削除)イベントは、元のラベルをソース、URI、値で参照します。否定が元のラベルより先に到着した場合(バックフィル中によくある)、黙って破棄されます。label_sync ツールは全ストリームをリプレイしてこれらをキャッチします。
Fjall キュー汚染: Fjall 組み込みデータベース(wintermute のキューに使用)は、クラッシュ後に「汚染」状態になり、すべてのキュー操作がブロックされる可能性があります。修正方法は、キューデータベースディレクトリを削除して再起動することです。wintermute はリレーのカーソルから追いつきます(リレーは約72時間の履歴を保持)。
TLS プロバイダの初期化: Rust の rustls は、TLS 接続の前に暗号プロバイダを明示的にインストールする必要があります。起動時に rustls::crypto::aws_lc_rs::default_provider().install_default() がないと、firehose への最初の WebSocket 接続がパニックになります。
アカウント移行後の署名鍵ローテーション: ユーザーが PDS 間を移行する際、署名鍵が変更されます。データプレーンは ID データを staleTTL 1時間でキャッシュします。その間、移行したユーザーの JWT 検証は失敗します。修正方法は、検証再試行時にキャッシュをバイパスし、PLC ディレクトリから直接解決することです。
フルネットワークの AppView(全ユーザー約4,200万、記録約185億)の実行に基づきます。
ストレージ内訳(概算、フルネットワーク):
| テーブルグループ |
|---|
小規模なコミュニティが部分的な AppView(コミュニティメンバーのみインデックス)を実行する場合、要件はインデックスされたアカウント数にほぼ比例します。
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
競合は通常、packages/bsky/src/data-plane/server/routes/ と packages/bsky/src/api/ で発生します。上流の変更とともに私たちの追加を保持して解決してください。
上流と同様: MIT および Apache 2.0 のデュアルライセンス。LICENSE-MIT.txt と LICENSE-APACHE.txt を参照してください。
| コンポーネント | ソース | 目的 |
|---|
| rsky-wintermute | blacksky-algorithms/rsky | Rust 製 firehose インデクサ: イベントを消費し、リポジトリをバックフィルし、レコードを PostgreSQL にインデックス |
| rsky-relay | blacksky-algorithms/rsky | ラベラーサービスからのモデレーションラベルを受信するための AT Protocol リレー |
| rsky-video | blacksky-algorithms/rsky | ビデオアップロードサービス: Bunny Stream CDN 経由でトランスコードし、blob 参照をユーザーの PDS にアップロード |
| bsky-dataplane | このリポジトリ (services/bsky) | PostgreSQL 上の gRPC データ層 |
| bsky-appview | このリポジトリ (services/bsky) | app.bsky.* XRPC エンドポイント用 HTTP API サーバー |
| Palomar | blacksky-algorithms/indigo | 全文検索: フォロワー数ブーストを使用してプロファイルと投稿を OpenSearch にインデックス |
| palomar-sync | blacksky-algorithms/rsky | PostgreSQL から OpenSearch へのフォロワー数と PageRank スコアの同期 |
| 変数 | 必須 | 説明 |
|---|
DB_PRIMARY_URL | はい | ?options=-csearch_path%3Dbsky 付きの PostgreSQL 接続文字列 |
DB_REPLICA_URL | いいえ | 読み取りレプリカ接続文字列 |
BSKY_DATAPLANE_PORT | いいえ | gRPC ポート (デフォルト 2585) |
BSKY_REDIS_HOST | いいえ | キャッシュ用の Redis ホスト:ポート (現在は無効のままにすることを推奨) |
BLACKSKY_MEMBERSHIP_DB_URL | いいえ | コミュニティメンバーシップ用の別 DB (Blacksky 固有) |
| 変数 | 必須 | 説明 |
|---|
BSKY_APPVIEW_PORT | いいえ | HTTP ポート (デフォルト 2584) |
BSKY_DATAPLANE_URLS | はい | カンマ区切りのデータプレーン gRPC URL |
BSKY_DID | はい | AppView の DID (例: did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | はい | Ozone モデレーションサービス DID |
BSKY_ADMIN_PASSWORDS | はい | 基本認証用のカンマ区切りの管理者パスワード |
| リソース | 最小 | 推奨 |
|---|
| CPU | 16 コア | 48+ コア |
| RAM | 64 GB | 256 GB |
| ストレージ | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | 専用、同一マシンまたは低レイテンシ | 同一マシンを推奨 |
| ネットワーク | 持続 100 Mbps | 1 Gbps+ |
| サイズ |
|---|
| 投稿 + レコード | ~3.5 TB |
| いいね | ~2 TB |
| フォロー | ~500 GB |
| 通知 | ~600 GB |
| インデックス | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |