
Fork der AT Protocol-Referenzimplementierung mit leistungsoptimierter AppView, Rust-basiertem Firehose-Indexer, Redis-Caching und Community-Funktionen für selbstgehostete soziale Netzwerke in großem Maßstab.
Dies ist der Fork des AT Protocol Reference Implementation von Blacksky, der von Bluesky Social PBC erstellt wurde. Er betreibt die AppView unter api.blacksky.community.
Wir veröffentlichen dies aus Transparenzgründen und damit andere Gemeinschaften von der Arbeit profitieren können. Dieses Repository akzeptiert keine Beiträge, Issues oder PRs. Wenn Sie die kanonische atproto-Implementierung wünschen, verwenden Sie bluesky-social/atproto.
Alle Änderungen befinden sich in packages/bsky (AppView-Logik), services/bsky (Laufzeitkonfiguration) und einer benutzerdefinierten Migration. Alles andere ist Upstream.
Die Upstream-Datenebene enthält einen TypeScript-Firehose-Consumer (subscription.ts), der Ereignisse direkt indiziert. Wir haben ihn aus mehreren Gründen durch rsky-wintermute, einen Rust-Indexer, ersetzt:
Die Datenebene und die AppView aus diesem Repository laufen weiterhin unverändert. Sie lesen aus der PostgreSQL-Datenbank, die Wintermute beschreibt. Wir starten nur das eingebaute Firehose-Abonnement nicht.
Diese sind allgemein für jeden nützlich, der eine AppView in großem Maßstab selbst hostet.
LATERAL-JOIN-Abfrageoptimierung (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline und getListFeed wurden mit PostgreSQL LATERAL JOINs umgeschrieben, um die Indexnutzung pro Benutzer zu erzwingen, anstatt vollständige Tabellenscans durchzuführen. Große Verbesserung für Benutzer, die Tausenden von Konten folgen.Redis-Caching-Layer (packages/bsky/src/data-plane/server/cache/)
Timestamp-Objekte nach JSON-Roundtrip durch Redis ihre .toDate()-Methode verlieren, was bei Cache-Treffern zu unvollständiger Profilhydrierung führt. Wir betreiben Redis-Caching derzeit deaktiviert. Die Lösung besteht darin, Zeitstempel beim Cache-Schreiben als ISO-Strings zu serialisieren und beim Lesen zu rekonstruieren.Serverseitige Durchsetzung von Benachrichtigungseinstellungen (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons angibt, wendet der Server die gespeicherten Benachrichtigungseinstellungen des Benutzers an. Ohne dies werden die Einstellungen nur clientseitig durchgesetzt und haben keine Wirkung.Auth-Verifier-Problem mit abgelaufenem Signaturschlüssel (packages/bsky/src/auth-verifier.ts)
forceRefresh) wird der In-Memory-Identity-Cache der Datenebene umgangen und das DID-Dokument direkt aus dem PLC-Verzeichnis aufgelöst. Behebt Authentifizierungsfehler nach Kontomigration, bei der der Signaturschlüssel rotiert, der Cache aber den alten Schlüssel enthält.JSON-Bereinigung (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) und Steuerzeichen aus gespeicherten Datensätzen vor der JSON-Analyse. Diese sind gemäß RFC 8259 gültig, werden aber von Node.js JSON.parse() abgelehnt, was zu stillen rowToRecord-Analysefehlern in der Datenebene führt, die sich als fehlende Posts äußern.Infrastruktur für private Community-Beiträge, die auf der AppView und nicht auf einzelnen PDSes leben. Spezifisch für die Funktionsweise von Blacksky, könnte aber als Referenz für andere Gemeinschaften dienen.
community.blacksky.feed.* mit Endpunkten zum Einreichen, Abrufen, Löschen, für Timeline und Thread-Ansichtencommunity_post-Tabelle (Migration: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 für gemischte Standard-/Community-Post-ThreadsBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(Rust-Indexer) | (Go-Suche)
- Firehose-Consumer | |
- Backfiller | v
- Label-Indexer | OpenSearch
- Direkt-Indexer |
v
bsky-dataplane (gRPC :2585) <--- Redis (optional)
|
v
bsky-appview (HTTP :2584)
|
v
Reverse-Proxy (Caddy/nginx)
Wintermute ist ein monolithischer Rust-Dienst mit vier parallelen Verarbeitungspfaden:
bsky.network-Firehose per WebSocket her, schreibt Ereignisse in Fjall (eingebetteter Key-Value-Store) WarteschlangenON CONFLICT für Idempotenz in PostgreSQLZusätzliche CLI-Werkzeuge im rsky-Repo:
queue_backfill – DIDs für Backfill aus CSV, PDS-Entdeckung oder direkten DID-Listen in die Warteschlange stellendirect_index – bestimmte Repos abrufen und indizieren, unter Umgehung der Warteschlangen (nützlich zur Reparatur einzelner Konten)label_sync – Label-Streams ab Cursor 0 wiedergeben, um verpasste Negationen nachzuholenplc_import – Bulk-Import von Handle/DID-Zuordnungen aus dem PLC-Verzeichnispalomar-sync – Follower-Anzahlen und PageRank mit OpenSearch synchronisierenVideo-Upload-Dienst für Benutzer, deren PDS Blueskys video.bsky.app nicht unterstützt. Verwendet eine eigene DID (did:web:video.blacksky.community), um sich über Dienst-Auth-JWTs bei Benutzer-PDSes zu authentifizieren. Ablauf:
Moderationslabels stammen von Labeler-Diensten (z.B. Blueskys Ozone) per WebSocket-Abonnement. Der Ingester von Wintermute verarbeitet Labels in einer dedizierten label_live-Warteschlange (geringes Volumen, getrennt vom Haupt-Firehose). Das Werkzeug label_sync kann den vollständigen Stream eines Labelers wiedergeben, um verpasste Negationen (Entfernungen von Labels) nachzuholen, ohne Labels erneut einzufügen.
bskyDas Schema bsky wird durch die Migrationen der Datenebene erstellt. Beim ersten Start wendet die Datenebene alle Migrationen automatisch an. Die einzige Blacksky-spezifische Migration ist 20260202T120000000Z-add-community-post.ts (Tabelle für Community-Beiträge). Wenn Sie keine Community-Beiträge benötigen, können Sie sie entfernen.
rsky-wintermute schreibt in dasselbe Schema. Alle INSERT-Anweisungen verwenden ON CONFLICT, daher ist es sicher, Wintermute und die Datenebenen-Migrationen in beliebiger Reihenfolge auszuführen.
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
Ein vollständiger Netzwerk-Backfill (alle ~42 Mio. Benutzer, ~18,5 Mrd. Datensätze) dauert selbst mit Wintermutes paralleler Verarbeitung Wochen. Erwarten Sie:
Während des Backfills ist die AppView funktionsfähig, zeigt aber unvollständige Daten für Benutzer, die noch nicht backgefillt wurden. Live-Ereignisse werden unabhängig vom Backfill-Fortschritt sofort indiziert.
Dies sind Probleme, auf die wir beim Aufsetzen einer AppView für das gesamte Netzwerk gestoßen sind. Wenn Sie dasselbe tun, werden Sie wahrscheinlich auf einige davon stoßen:
COPY-Textformat-JSON-Korruption: Das COPY-Textprotokoll von PostgreSQL behandelt Backslash als Escape-Zeichen. Wenn Ihr Bulk-Loader Backslashes in JSON-Strings nicht escaped, wird \" zu " und Sie erhalten stillschweigend korrupte Datensätze. Die Spalte record.json ist vom Typ text (nicht jsonb), daher wird PostgreSQL dies nicht abfangen. Wir haben etwa 66.000 korrupte Datensätze gefunden und mussten sie durch erneutes Abrufen über die öffentliche API reparieren.
Nullbytes in JSON: Einige AT Protocol-Datensätze enthalten \u0000 (Nullbyte), was gemäß RFC 8259 gültiges JSON ist, aber von Node.js JSON.parse() abgelehnt wird. Die Datenebene gibt für diese Datensätze stillschweigend null zurück. Entfernen Sie Nullbytes, bevor Sie in die Datenbank schreiben.
Zeitstempel-Format-Empfindlichkeit: Die Datenebene erwartet Zeitstempel mit Millisekundengenauigkeit und Z-Suffix (2026-01-12T19:45:23.307Z). Nanosekundengenauigkeit oder Zeitzonenoffset-Format (+00:00) verursachen subtile Sortierungs- und Vergleichsprobleme.
Aufblähung der Benachrichtigungstabelle: Ohne eine eindeutige Einschränkung für (did, recordUri, reason) wächst die Benachrichtigungstabelle unbegrenzt mit Duplikaten. Unsere erreichte 1,3 Milliarden Zeilen (663 GB), bevor wir es bemerkten. Das Hinzufügen von ON CONFLICT DO NOTHING zu INSERTs hilft nur, wenn der eindeutige Index zuerst existiert, und das Erstellen des Index erfordert die Deduplizierung der vorhandenen Daten.
Post-Einbettungstabellen: Die Tabellen post_embed_image und post_embed_video werden standardmäßig nicht gefüllt, wenn Ihr Indexer sie nicht verarbeitet. Ohne diese liefert der Medienfilter auf getAuthorFeed nichts zurück. Diese müssen separat backgefillt werden.
Label-Negations-Reihenfolge: Label-Negations-Ereignisse (Entfernung) verweisen auf das ursprüngliche Label nach Quelle, URI und Wert. Wenn Negationen vor dem ursprünglichen Label eintreffen (häufig während Backfills), werden sie stillschweigend verworfen. Das Werkzeug label_sync spielt den gesamten Stream ab, um diese zu erfassen.
Fjall-Warteschlangen-Vergiftung: Die eingebettete Fjall-Datenbank (für Wintermutes Warteschlangen) kann nach Abstürzen in einen "vergifteten" Zustand geraten, der alle Warteschlangenoperationen blockiert. Die Lösung besteht darin, das Warteschlangen-Datenbankverzeichnis zu löschen und neu zu starten – Wintermute holt den Stand vom Relay-Cursor auf (Relays behalten etwa 72 Stunden Verlauf).
TLS-Provider-Initialisierung: Rusts rustls erfordert die explizite Installation eines Crypto-Providers vor jeder TLS-Verbindung. Ohne rustls::crypto::aws_lc_rs::default_provider().install_default() beim Start führt die erste WebSocket-Verbindung zum Firehose zu einer Panik.
Signaturschlüssel-Rotation nach Kontomigration: Wenn Benutzer zwischen PDSes migrieren, ändert sich ihr Signaturschlüssel. Die Datenebene speichert Identitätsdaten mit einem staleTTL von 1 Stunde zwischen. Während dieses Fensters schlägt die JWT-Überprüfung für migrierte Benutzer fehl. Die Lösung besteht darin, den Cache bei der Überprüfungswiederholung zu umgehen und direkt aus dem PLC-Verzeichnis aufzulösen.
Basierend auf dem Betrieb einer AppView für das gesamte Netzwerk (alle ~42 Mio. Benutzer, ~18,5 Mrd. Datensätze).
Speicheraufteilung (ungefähr, gesamtes Netzwerk):
Für eine kleinere Community, die eine teilweise AppView betreibt (nur Community-Mitglieder indiziert), skalieren die Anforderungen grob linear mit den indizierten Konten.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
Konflikte treten typischerweise in packages/bsky/src/data-plane/server/routes/ und packages/bsky/src/api/ auf. Lösen Sie diese, indem Sie unsere Ergänzungen neben den Upstream-Änderungen behalten.
Gleich wie Upstream: dual-lizenziert unter MIT und Apache 2.0. Siehe LICENSE-MIT.txt und LICENSE-APACHE.txt.
| Komponente | Quelle | Zweck |
|---|
| rsky-wintermute | blacksky-algorithms/rsky | Rust-Firehose-Indexer: verarbeitet Ereignisse, backfillt Repos, indiziert Datensätze in PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | AT Protocol Relay zum Empfangen von Moderationslabels von Labeler-Diensten |
| rsky-video | blacksky-algorithms/rsky | Video-Upload-Dienst: transkodiert über Bunny Stream CDN, lädt Blob-Referenzen auf Benutzer-PDSes hoch |
| bsky-dataplane | Dieses Repo (services/bsky) | gRPC-Datenebene über PostgreSQL |
| bsky-appview | Dieses Repo (services/bsky) | HTTP-API-Server für app.bsky.* XRPC-Endpunkte |
| Palomar | blacksky-algorithms/indigo | Volltextsuche: indiziert Profile und Posts in OpenSearch mit Follower-Anzahl-Boosting |
| palomar-sync | blacksky-algorithms/rsky | Synchronisiert Follower-Anzahlen und PageRank-Scores von PostgreSQL nach OpenSearch |
| Variable | Erforderlich | Beschreibung |
|---|
DB_PRIMARY_URL | Ja | PostgreSQL-Verbindungsstring mit ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | Nein | Verbindungsstring für Read-Replica |
BSKY_DATAPLANE_PORT | Nein | gRPC-Port (Standard 2585) |
BSKY_REDIS_HOST | Nein | Redis-Host:Port für Caching (derzeit wird empfohlen, es deaktiviert zu lassen) |
BLACKSKY_MEMBERSHIP_DB_URL | Nein | Separate DB für Community-Mitgliedschaft (Blacksky-spezifisch) |
| Variable | Erforderlich | Beschreibung |
|---|
BSKY_APPVIEW_PORT | Nein | HTTP-Port (Standard 2584) |
BSKY_DATAPLANE_URLS | Ja | Kommagetrennte gRPC-URLs der Datenebene |
BSKY_DID | Ja | Die DID der AppView (z.B. did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Ja | DID des Ozone-Moderationsdienstes |
BSKY_ADMIN_PASSWORDS | Ja | Kommagetrennte Admin-Passwörter für Basic Auth |
| Ressource | Minimum | Empfohlen |
|---|
| CPU | 16 Kerne | 48+ Kerne |
| RAM | 64 GB | 256 GB |
| Speicher | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | Dediziert, gleicher Rechner oder niedrige Latenz | Gleicher Rechner empfohlen |
| Netzwerk | Anhaltende 100 Mbit/s | 1 Gbit/s+ |
| Tabellengruppe | Größe |
|---|
| Posts + Datensätze | ~3,5 TB |
| Likes | ~2 TB |
| Folgt | ~500 GB |
| Benachrichtigungen | ~600 GB |
| Indizes | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |