
Fork dell'implementazione di riferimento del protocollo AT con AppView ottimizzato per le prestazioni, indicizzatore firehose basato su Rust, caching Redis e funzionalità community per social networking self-hostato su larga scala.
Questa è la fork di Blacksky dell'implementazione di riferimento del AT Protocol di Bluesky Social PBC. Alimenta l'AppView su api.blacksky.community.
Pubbliciamo questo codice per trasparenza e affinché altre comunità possano beneficiare del lavoro. Questo repository non accetta contributi, issue o PR. Se desideri l'implementazione atproto canonica, utilizza bluesky-social/atproto.
Tutte le modifiche sono in packages/bsky (logica dell'AppView), services/bsky (configurazione runtime) e una migrazione personalizzata. Tutto il resto è upstream.
Il dataplane upstream include un consumatore di firehose TypeScript (subscription.ts) che indicizza gli eventi direttamente. Lo abbiamo sostituito con rsky-wintermute, un indicizzatore Rust, per diverse ragioni:
Il dataplane e l'appview di questo repository funzionano ancora così come sono. Leggono dal database PostgreSQL che wintermute scrive. Semplicemente non avviamo la sottoscrizione al firehose integrata.
Queste sono ampiamente utili per chiunque ospiti un AppView su larga scala.
Ottimizzazione delle query LATERAL JOIN (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline e getListFeed riscritti con LATERAL JOIN di PostgreSQL per forzare l'utilizzo degli indici per utente invece di scansioni complete delle tabelle. Miglioramento significativo per utenti che seguono migliaia di account.Livello di cache Redis (packages/bsky/src/data-plane/server/cache/)
Timestamp perdono il metodo .toDate() dopo il round-trip JSON attraverso Redis, causando un'idratazione incompleta del profilo in caso di cache hit. Attualmente eseguiamo con la cache Redis disabilitata. La soluzione è serializzare i timestamp come stringhe ISO durante la scrittura nella cache e ricostruirli in fase di lettura.Applicazione lato server delle preferenze di notifica (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons, il server applica le preferenze di notifica salvate dell'utente. Senza questa funzione, le preferenze vengono applicate solo lato client e non hanno effetto.Risoluzione della chiave di firma scaduta nel verificatore di autenticazione (packages/bsky/src/auth-verifier.ts)
forceRefresh, bypassa la cache di identità in memoria del dataplane e risolve il documento DID direttamente dalla directory PLC. Risolve errori di autenticazione dopo la migrazione dell'account quando la chiave di firma viene ruotata ma la cache mantiene la vecchia chiave.Sanitizzazione JSON (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) e i caratteri di controllo dai record memorizzati prima del parsing JSON. Questi sono validi secondo RFC 8259 ma vengono rifiutati da JSON.parse() di Node.js, causando errori silenziosi di parsing rowToRecord nel dataplane che si manifestano come post mancanti.Infrastruttura per post privati di comunità che risiedono sull'AppView anziché su singoli PDS. Specifica per il funzionamento di Blacksky, ma potrebbe essere di riferimento per altre comunità.
community.blacksky.feed.* con endpoint per submit, get, delete, timeline e visualizzazioni threadcommunity_post separata (migrazione: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 per thread misti di post standard e comunitariBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(Indicizzatore Rust) | (Ricerca Go)
- consumatore firehose | |
- backfiller | v
- indicizzatore etichette | OpenSearch
- indicizzatore diretto |
v
bsky-dataplane (gRPC :2585) <--- Redis (opzionale)
|
v
bsky-appview (HTTP :2584)
|
v
Reverse proxy (Caddy/nginx)
Wintermute è un servizio Rust monolitico con quattro percorsi di elaborazione paralleli:
bsky.network tramite WebSocket, scrive eventi nelle code Fjall (key-value store embedded)ON CONFLICT per idempotenzaStrumenti CLI aggiuntivi inclusi nel repository rsky:
queue_backfill — accoda DID per backfill da CSV, scoperta PDS o elenchi diretti di DIDdirect_index — recupera e indicizza repository specifici bypassando le code (utile per correggere account individuali)label_sync — riproduce il flusso delle etichette dal cursore 0 per recuperare negazioni mancateplc_import — importa bulk le mappature handle/DID dalla directory PLCpalomar-sync — sincronizza i conteggi follower e i punteggi PageRank in OpenSearchServizio di upload video per utenti il cui PDS non supporta video.bsky.app di Bluesky. Utilizza un proprio DID (did:web:video.blacksky.community) per autenticarsi ai PDS degli utenti tramite JWT di autenticazione del servizio. Flusso:
Le etichette di moderazione provengono dai servizi labeler (es. Ozone di Bluesky) tramite sottoscrizione WebSocket. L'ingester di Wintermute elabora le etichette in una coda dedicata label_live (basso volume, separata dal firehose principale). Lo strumento label_sync può riprodurre il flusso completo di un labeler per recuperare le negazioni mancate (rimozione etichette) senza reinserire le etichette.
bskyLo schema bsky viene creato dalle migrazioni del dataplane. Al primo avvio, il dataplane applica automaticamente tutte le migrazioni. L'unica migrazione specifica di Blacksky è 20260202T120000000Z-add-community-post.ts (tabella dei post della comunità). Se non hai bisogno dei post della comunità, puoi rimuoverla.
rsky-wintermute scrive su questo stesso schema. Tutte le sue istruzioni INSERT utilizzano ON CONFLICT, quindi è sicuro eseguire wintermute e le migrazioni del dataplane in qualsiasi ordine.
pnpm install
pnpm build
node services/bsky/dataplane.js
node services/bsky/api.js
Un backfill completo della rete (tutti ~42M utenti, ~18,5 miliardi di record) richiede settimane anche con l'elaborazione parallela di wintermute. Prevedi:
Durante il backfill, l'AppView è funzionale ma mostrerà dati incompleti per gli utenti non ancora sottoposti a backfill. Gli eventi live vengono indicizzati immediatamente indipendentemente dall'avanzamento del backfill.
Questi sono problemi che abbiamo incontrato avviando un AppView di rete completa. Se stai facendo lo stesso, probabilmente ne incontrerai alcuni:
Corruzione JSON nel formato di testo COPY: Il protocollo di testo COPY di PostgreSQL tratta il backslash come carattere di escape. Se il tuo caricatore bulk non escape i backslash nelle stringhe JSON, \" diventa " e ottieni record corrotti silenziosamente. La colonna record.json è di tipo text (non jsonb), quindi PostgreSQL non lo rileverà. Abbiamo trovato ~66.000 record corrotti e abbiamo dovuto ripararli recuperandoli dall'API pubblica.
Byte nulli nel JSON: Alcuni record del AT Protocol contengono \u0000 (byte nullo), che è JSON valido secondo RFC 8259 ma rifiutato da JSON.parse() di Node.js. Il dataplane restituisce silenziosamente null per questi record. Rimuovi i byte nulli prima di scrivere nel database.
Sensibilità al formato del timestamp: Il dataplane si aspetta timestamp con precisione al millisecondo e suffisso Z (2026-01-12T19:45:23.307Z). La precisione al nanosecondo o il formato con offset fuso orario (+00:00) causano sottili problemi di ordinamento e confronto.
Gonfiamento della tabella delle notifiche: Senza un vincolo univoco su (did, recordUri, reason), la tabella delle notifiche cresce senza limiti con duplicati. La nostra ha raggiunto 1,3 miliardi di righe (663 GB) prima che lo scoprissimo. Aggiungere ON CONFLICT DO NOTHING agli INSERT aiuta solo se l'indice univoco esiste già, e creare l'indice richiede la deduplicazione dei dati esistenti.
Tabelle degli embed dei post: Le tabelle post_embed_image e post_embed_video non vengono popolate di default se il tuo indicizzatore non le gestisce. Senza di esse, il filtro multimediale su getAuthorFeed non restituisce nulla. Queste devono essere sottoposte a backfill separatamente.
Ordinamento delle negazioni delle etichette: La negazione (rimozione) di un'etichetta fa riferimento all'etichetta originale per sorgente, URI e valore. Se le negazioni arrivano prima dell'etichetta originale (comune durante il backfill), vengono scartate silenziosamente. Lo strumento label_sync riproduce il flusso completo per recuperare queste situazioni.
Avvelenamento della coda Fjall: Il database embedded Fjall (usato per le code di wintermute) può entrare in uno stato "avvelenato" dopo crash, bloccando tutte le operazioni sulle code. La soluzione è eliminare la directory del database della coda e riavviare – wintermute recupererà dal cursore del relay (i relay mantengono circa 72 ore di storia).
Inizializzazione del provider TLS: rustls di Rust richiede l'installazione esplicita di un provider crittografico prima di qualsiasi connessione TLS. Senza rustls::crypto::aws_lc_rs::default_provider().install_default() all'avvio, la prima connessione WebSocket al firehose fallisce con panico.
Rotazione della chiave di firma dopo la migrazione dell'account: Quando gli utenti migrano tra PDS, la loro chiave di firma cambia. Il dataplane memorizza nella cache i dati di identità con uno staleTTL di 1 ora. Durante questa finestra, la verifica JWT fallisce per gli utenti migrati. La soluzione è bypassare la cache durante i retry di verifica e risolvere direttamente dalla directory PLC.
Basati sull'esecuzione di un AppView di rete completa (tutti ~42M utenti, ~18,5 miliardi di record).
Ripartizione dell'archiviazione (approssimativo, rete completa):
Per una comunità più piccola che esegue un AppView parziale (indicizzando solo i membri della comunità), i requisiti scalano approssimativamente in modo lineare con il numero di account indicizzati.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
I conflitti saranno tipicamente in packages/bsky/src/data-plane/server/routes/ e packages/bsky/src/api/. Risolvi mantenendo le nostre aggiunte insieme alle modifiche upstream.
Stessa dell'upstream: dual-licensed sotto MIT e Apache 2.0. Vedi LICENSE-MIT.txt e LICENSE-APACHE.txt.
| Componente | Sorgente | Scopo |
|---|
| rsky-wintermute | blacksky-algorithms/rsky | Indicizzatore Rust: consuma eventi, esegue backfill dei repository, indicizza i record in PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | Relay AT Protocol per ricevere etichette di moderazione dai servizi labeler |
| rsky-video | blacksky-algorithms/rsky | Servizio di upload video: transcodifica tramite Bunny Stream CDN, carica i riferimenti blob nei PDS degli utenti |
| bsky-dataplane | Questo repository (services/bsky) | Livello dati gRPC su PostgreSQL |
| bsky-appview | Questo repository (services/bsky) | Server API HTTP per gli endpoint XRPC app.bsky.* |
| Palomar | blacksky-algorithms/indigo | Ricerca full-text: indicizza profili e post in OpenSearch con potenziamento del conteggio follower |
| palomar-sync | blacksky-algorithms/rsky | Sincronizza conteggi follower e punteggi PageRank da PostgreSQL a OpenSearch |
| Variabile | Obbligatoria | Descrizione |
|---|
DB_PRIMARY_URL | Sì | Stringa di connessione PostgreSQL con ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | No | Stringa di connessione per replica in lettura |
BSKY_DATAPLANE_PORT | No | Porta gRPC (default 2585) |
BSKY_REDIS_HOST | No | Host:port Redis per caching (attualmente si consiglia di lasciarlo disabilitato) |
BLACKSKY_MEMBERSHIP_DB_URL | No | DB separato per l'appartenenza alla comunità (specifico Blacksky) |
| Variabile | Obbligatoria | Descrizione |
|---|
BSKY_APPVIEW_PORT | No | Porta HTTP (default 2584) |
BSKY_DATAPLANE_URLS | Sì | URL gRPC del dataplane separati da virgole |
BSKY_DID | Sì | DID dell'AppView (es. did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Sì | DID del servizio di moderazione Ozone |
BSKY_ADMIN_PASSWORDS | Sì | Password amministrative separate da virgole per autenticazione di base |
| Risorsa | Minimo | Consigliato |
|---|
| CPU | 16 core | 48+ core |
| RAM | 64 GB | 256 GB |
| Archiviazione | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | Dedicato, stessa macchina o bassa latenza | Si consiglia stessa macchina |
| Rete | 100 Mbps sostenuti | 1 Gbps+ |
| Gruppo di tabelle | Dimensione |
|---|
| Post + record | ~3.5 TB |
| Mi piace | ~2 TB |
| Seguiti | ~500 GB |
| Notifiche | ~600 GB |
| Indici | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |