
Fork de l'implémentation de référence du protocole AT avec un AppView optimisé pour les performances, un indexeur firehose basé sur Rust, un cache Redis et des fonctionnalités communautaires pour un réseau social auto-hébergé à grande échelle.
Voici le fork de Blacksky de l'implémentation de référence du protocole AT par Bluesky Social PBC. Il alimente l’AppView à l’adresse api.blacksky.community.
Nous publions ce code par souci de transparence et pour que d’autres communautés puissent bénéficier de ce travail. Ce dépôt n’accepte ni contributions, ni issues, ni PRs. Si vous souhaitez l’implémentation canonique d’atproto, utilisez bluesky-social/atproto.
Toutes les modifications se trouvent dans packages/bsky (logique AppView), services/bsky (configuration d’exécution) et une migration personnalisée. Tout le reste provient de l’amont.
Le dataplane en amont inclut un consumer Firehose TypeScript (subscription.ts) qui indexe les événements directement. Nous l’avons remplacé par rsky-wintermute, un indexeur Rust, pour plusieurs raisons :
Le dataplane et l’appview de ce dépôt fonctionnent toujours tels quels. Ils lisent depuis la base de données PostgreSQL que wintermute écrit. Nous ne démarrons simplement pas l’abonnement Firehose intégré.
Ces correctifs sont largement utiles à toute personne hébergeant elle-même une AppView à grande échelle.
Optimisation des requêtes LATERAL JOIN (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline et getListFeed réécrites avec des LATERAL JOIN PostgreSQL pour forcer l’utilisation d’index par utilisateur au lieu de parcourir toutes les tables. Amélioration majeure pour les utilisateurs qui suivent des milliers de comptes.Couche de cache Redis (packages/bsky/src/data-plane/server/cache/)
Timestamp perdent leur méthode .toDate() après un aller-retour JSON via Redis, ce qui provoque une hydration incomplète du profil lors d’un hit dans le cache. Nous utilisons actuellement le cache Redis désactivé. La correction consiste à sérialiser les timestamps en chaînes ISO lors de l’écriture dans le cache et à les reconstruire à la lecture.Application côté serveur des préférences de notification (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons, le serveur applique les préférences de notification enregistrées par l’utilisateur. Sans cela, les préférences ne sont appliquées que côté client et restent sans effet.Correction de la clé de signature obsolète dans le vérificateur d’authentification (packages/bsky/src/auth-verifier.ts)
forceRefresh), contourne le cache d’identité en mémoire du dataplane et résout le document DID directement depuis le répertoire PLC. Corrige les échecs d’authentification après une migration de compte où la clé de signature change mais le cache conserve l’ancienne clé.Nettoyage JSON (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) et les caractères de contrôle des enregistrements stockés avant le parsing JSON. Ces caractères sont valides selon RFC 8259 mais rejetés par JSON.parse() de Node.js, provoquant des échecs silencieux de parsing rowToRecord dans le dataplane, qui se manifestent par des publications manquantes.Infrastructure pour les publications privées de communauté qui résident sur l’AppView plutôt que sur des PDS individuels. Spécifique au fonctionnement de Blacksky, mais peut servir de référence pour d’autres communautés.
community.blacksky.feed.* avec points de terminaison pour soumettre, obtenir, supprimer, timeline et fils de discussioncommunity_post séparée (migration : 20260202T120000000Z-add-community-post.ts)getPostThreadV2 pour des fils mixtes publications standard/communautairesBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(Indexeur Rust) | (Recherche Go)
- consumer firehose | |
- backfilleur | v
- indexeur de labels | OpenSearch
- indexeur direct |
v
bsky-dataplane (gRPC :2585) <--- Redis (optionnel)
|
v
bsky-appview (HTTP :2584)
|
v
Proxy inverse (Caddy/nginx)
Wintermute est un service Rust monolithique avec quatre chemins de traitement parallèles :
bsky.network via WebSocket, écrit les événements dans les files d’attente Fjall (magasin clé-valeur intégré)ON CONFLICT pour l’idempotenceOutils CLI supplémentaires inclus dans le dépôt rsky :
queue_backfill – met en file d’attente les DIDs pour backfill à partir d’un CSV, de la découverte de PDS ou de listes directes de DIDsdirect_index – récupère et indexe des dépôts spécifiques en contournant les files d’attente (utile pour corriger des comptes individuels)label_sync – rejoue les flux de labels à partir du curseur 0 pour rattraper les annulations manquéesplc_import – importe en masse les mappages handle/DID depuis le répertoire PLCpalomar-sync – synchronise les compteurs d’abonnés et PageRank vers OpenSearchService de téléchargement vidéo pour les utilisateurs dont le PDS ne prend pas en charge video.bsky.app de Bluesky. Utilise son propre DID (did:web:video.blacksky.community) pour s’authentifier auprès des PDS utilisateur via des JWT d’authentification de service. Flux :
Les labels de modération proviennent des services de labellisation (par exemple, Ozone de Bluesky) via abonnement WebSocket. L’ingester de Wintermute traite les labels dans une file dédiée label_live (faible volume, séparée du firehose principal). L’outil label_sync peut rejouer le flux complet d’un service de labellisation pour rattraper les annulations manquées (suppressions de labels) sans réinsérer les labels.
bskyLe schéma bsky est créé par les migrations du dataplane. Lors de la première exécution, le dataplane applique toutes les migrations automatiquement. La seule migration spécifique à Blacksky est 20260202T120000000Z-add-community-post.ts (table des publications communautaires). Si vous n’avez pas besoin des publications communautaires, vous pouvez la supprimer.
rsky-wintermute écrit dans ce même schéma. Toutes ses instructions INSERT utilisent ON CONFLICT, il est donc sûr d’exécuter wintermute et les migrations du dataplane dans n’importe quel ordre.
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
Un backfill complet du réseau (tous les ~42M d’utilisateurs, ~18,5Mds d’enregistrements) prend des semaines même avec le traitement parallèle de wintermute. Attendez-vous à :
Pendant le backfill, l’AppView est fonctionnelle mais affichera des données incomplètes pour les utilisateurs qui n’ont pas encore été backfillés. Les événements en direct sont indexés immédiatement quel que soit l’avancement du backfill.
Voici les problèmes rencontrés lors de l’amorçage d’une AppView réseau complet. Si vous faites de même, vous rencontrerez probablement certains d’entre eux :
Corruption JSON dans le format texte COPY : Le protocole texte COPY de PostgreSQL traite l’antislash comme un caractère d’échappement. Si votre chargeur en masse n’échappe pas les antislashs dans les chaînes JSON, \" devient " et vous obtenez des enregistrements silencieusement corrompus. La colonne record.json est de type text (pas jsonb), donc PostgreSQL ne le détectera pas. Nous avons trouvé environ 66 000 enregistrements corrompus et avons dû les réparer en les récupérant via l’API publique.
Octets nuls dans le JSON : Certains enregistrements du protocole AT contiennent \u0000 (octet nul), qui est un JSON valide selon RFC 8259 mais rejeté par JSON.parse() de Node.js. Le dataplane retourne silencieusement null pour ces enregistrements. Supprimez les octets nuls avant d’écrire dans la base de données.
Sensibilité au format des timestamps : Le dataplane s’attend à des timestamps avec une précision milliseconde et un suffixe Z (2026-01-12T19:45:23.307Z). Une précision nanoseconde ou un format de décalage horaire (+00:00) provoque des problèmes subtils de tri et de comparaison.
Gonflement de la table des notifications : Sans contrainte unique sur (did, recordUri, reason), la table des notifications croît de manière illimitée avec des doublons. La nôtre a atteint 1,3 milliard de lignes (663 Go) avant que nous ne nous en rendions compte. Ajouter ON CONFLICT DO NOTHING aux INSERT n’aide que si l’index unique existe d’abord, et créer l’index nécessite une déduplication des données existantes.
Tables d’intégration des publications : Les tables post_embed_image et post_embed_video ne sont pas remplies par défaut si votre indexeur ne les gère pas. Sans elles, le filtre médias sur getAuthorFeed ne renvoie rien. Elles doivent être backfillées séparément.
Ordre d’annulation des labels : Les événements d’annulation de label (suppression) référencent le label d’origine par source, URI et valeur. Si les annulations arrivent avant le label d’origine (fréquent lors du backfill), elles sont silencieusement ignorées. L’outil label_sync rejoue le flux complet pour les rattraper.
Empoisonnement de la file d’attente Fjall : La base de données intégrée Fjall (utilisée pour les files d’attente de wintermute) peut entrer dans un état « empoisonné » après des plantages, bloquant toutes les opérations sur les files. La solution est de supprimer le répertoire de la base de données des files et de redémarrer – wintermute rattrapera le retard à partir du curseur du relais (les relais conservent environ 72 heures d’historique).
Initialisation du fournisseur TLS : Le rustls de Rust nécessite d’installer explicitement un fournisseur cryptographique avant toute connexion TLS. Sans rustls::crypto::aws_lc_rs::default_provider().install_default() au démarrage, la première connexion WebSocket au firehose panique.
Rotation de la clé de signature après migration de compte : Lorsque les utilisateurs migrent entre PDS, leur clé de signature change. Le dataplane met en cache les données d’identité avec un staleTTL de 1 heure. Pendant cette fenêtre, la vérification JWT échoue pour les utilisateurs migrés. La correction consiste à contourner le cache lors d’une nouvelle tentative de vérification et à résoudre directement depuis le répertoire PLC.
Basé sur l’exécution d’une AppView réseau complet (tous les ~42M d’utilisateurs, ~18,5Mds d’enregistrements).
Détail du stockage (approximatif, réseau complet) :
Pour une communauté plus petite exécutant une AppView partielle (indexant uniquement les membres de la communauté), les besoins évoluent à peu près linéairement avec le nombre de comptes indexés.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
Les conflits se trouveront généralement dans packages/bsky/src/data-plane/server/routes/ et packages/bsky/src/api/. Résolvez-les en conservant nos ajouts à côté des modifications amont.
Idem que l’amont : double licence sous MIT et Apache 2.0. Voir LICENSE-MIT.txt et LICENSE-APACHE.txt.
| Composant | Source | Objectif |
|---|
| rsky-wintermute | blacksky-algorithms/rsky | Indexeur Firehose Rust : consomme les événements, backfill des dépôts, indexe les enregistrements dans PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | Relais du protocole AT pour recevoir les labels de modération des services de labellisation |
| rsky-video | blacksky-algorithms/rsky | Service de téléchargement vidéo : transcodage via Bunny Stream CDN, télécharge les références de blobs vers les PDS des utilisateurs |
| bsky-dataplane | Ce dépôt (services/bsky) | Couche de données gRPC sur PostgreSQL |
| bsky-appview | Ce dépôt (services/bsky) | Serveur d’API HTTP pour les points de terminaison XRPC app.bsky.* |
| Palomar | blacksky-algorithms/indigo | Recherche en texte intégral : indexe les profils et publications dans OpenSearch avec pondération basée sur le nombre d’abonnés |
| palomar-sync | blacksky-algorithms/rsky | Synchronise les compteurs d’abonnés et les scores PageRank de PostgreSQL vers OpenSearch |
| Variable | Requise | Description |
|---|
DB_PRIMARY_URL | Oui | Chaîne de connexion PostgreSQL avec ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | Non | Chaîne de connexion pour la réplica en lecture |
BSKY_DATAPLANE_PORT | Non | Port gRPC (par défaut 2585) |
BSKY_REDIS_HOST | Non | Hôte:port Redis pour le cache (actuellement recommandé de laisser désactivé) |
BLACKSKY_MEMBERSHIP_DB_URL | Non | Base de données séparée pour l’appartenance communautaire (spécifique à Blacksky) |
| Variable | Requise | Description |
|---|
BSKY_APPVIEW_PORT | Non | Port HTTP (par défaut 2584) |
BSKY_DATAPLANE_URLS | Oui | URLs gRPC du dataplane séparées par des virgules |
BSKY_DID | Oui | DID de l’AppView (par exemple did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Oui | DID du service de modération Ozone |
BSKY_ADMIN_PASSWORDS | Oui | Mots de passe administrateur séparés par des virgules pour l’authentification de base |
| Ressource | Minimum | Recommandé |
|---|
| CPU | 16 cœurs | 48+ cœurs |
| RAM | 64 Go | 256 Go |
| Stockage | 10 To NVMe | 28+ To NVMe (RAID) |
| PostgreSQL | Dédié, sur la même machine ou à faible latence | Même machine recommandée |
| Réseau | 100 Mbps soutenu | 1 Gbps+ |
| Groupe de tables | Taille |
|---|
| Publications + enregistrements | ~3,5 To |
| J’aime | ~2 To |
| Abonnements | ~500 Go |
| Notifications | ~600 Go |
| Index | ~4 To |
| OpenSearch (Palomar) | ~500 Go |