
Fork da implementação de referência do AT Protocol com AppView otimizado para desempenho, indexador de firehose baseado em Rust, cache Redis e funcionalidades comunitárias para redes sociais auto-hospedadas em escala.
Esta é uma bifurcação (fork) do AT Protocol reference implementation pela Blacksky's, mantida pela Bluesky Social PBC. Ela alimenta a AppView em api.blacksky.community.
Estamos publicando isso por transparência e para que outras comunidades possam se beneficiar do trabalho. Este repositório não aceita contribuições, issues ou PRs. Se você deseja a implementação canônica do atproto, use bluesky-social/atproto.
Todas as alterações estão em packages/bsky (lógica da AppView), services/bsky (configuração de runtime) e uma migração personalizada. Todo o resto é upstream.
O dataplane upstream inclui um consumidor de firehose em TypeScript (subscription.ts) que indexa eventos diretamente. Nós o substituímos por , um indexador em Rust, por várias razões:
O dataplane e a appview deste repositório ainda funcionam como estão. Eles leem do banco de dados PostgreSQL que o wintermute escreve. Apenas não iniciamos a assinatura de firehose embutida.
Estas são amplamente úteis para qualquer pessoa que esteja auto-hospedando uma AppView em escala.
Otimização de consulta LATERAL JOIN (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline e getListFeed foram reescritos com LATERAL JOINs do PostgreSQL para forçar o uso de índice por usuário em vez de varreduras completas de tabela. Melhoria significativa para usuários que seguem milhares de contas.Camada de cache Redis (packages/bsky/src/data-plane/server/cache/)
Timestamp perdem seu método .toDate() após ida e volta JSON pelo Redis, causando hidratação incompleta de perfil em acertos de cache. Atualmente executamos com o cache Redis desabilitado. A correção é serializar timestamps como strings ISO na escrita do cache e reconstruir na leitura.Aplicação no lado do servidor das preferências de notificação (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons, o servidor aplica as preferências de notificação salvas do usuário. Sem isso, as preferências são aplicadas apenas no lado do cliente e não têm efeito.Correção de chave de assinatura obsoleta no verificador de autenticação (packages/bsky/src/auth-verifier.ts)
forceRefresh), ignora o cache de identidade em memória do dataplane e resolve o documento DID diretamente do diretório PLC. Corrige falhas de autenticação após migração de conta onde a chave de assinatura é rotacionada, mas o cache mantém a chave antiga.Sanitização JSON (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) e caracteres de controle dos registros armazenados antes da análise JSON. Estes são válidos de acordo com RFC 8259, mas rejeitados pelo JSON.parse() do Node.js, causando falhas silenciosas de análise rowToRecord no dataplane que se manifestam como postagens ausentes.Infraestrutura para postagens privadas de comunidade que residem na AppView em vez de PDSes individuais. Específico de como o Blacksky funciona, mas pode servir como referência para outras comunidades.
community.blacksky.feed.* com endpoints para submit, get, delete, timeline e visualizações de threadcommunity_post (migração: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 para threads mistas de postagens padrão/comunitáriasBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(indexador Rust) | (busca em Go)
- consumidor firehose | |
- backfiller | v
- indexador de labels | OpenSearch
- indexador direto |
v
bsky-dataplane (gRPC :2585) <--- Redis (opcional)
|
v
bsky-appview (HTTP :2584)
|
v
Proxy reverso (Caddy/nginx)
| Componente | Origem | Propósito |
|---|---|---|
| rsky-wintermute | blacksky-algorithms/rsky | Indexador de firehose em Rust: consome eventos, faz backfill de repositórios, indexa registros no PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | Relay do AT Protocol para receber labels de moderação de serviços labeler |
| rsky-video | blacksky-algorithms/rsky | Serviço de upload de vídeo: transcodifica via Bunny Stream CDN, envia referências de blob para PDSes do usuário |
| bsky-dataplane | Este repositório (services/bsky) | Camada de dados gRPC sobre PostgreSQL |
| bsky-appview | Este repositório (services/bsky) | Servidor HTTP API para endpoints XRPC de app.bsky.* |
| Palomar | blacksky-algorithms/indigo | Busca em texto completo: indexa perfis e postagens no OpenSearch com boosting por contagem de seguidores |
| palomar-sync | blacksky-algorithms/rsky | Sincroniza contagens de seguidores e pontuações PageRank do PostgreSQL para o OpenSearch |
Wintermute é um serviço monolítico em Rust com quatro caminhos de processamento paralelo:
bsky.network via WebSocket, escreve eventos em filas Fjall (armazenamento chave-valor incorporado)ON CONFLICT para idempotênciaFerramentas CLI adicionais incluídas no repositório rsky:
queue_backfill -- enfileira DIDs para backfill a partir de CSV, descoberta de PDS ou listas diretas de DIDsdirect_index -- busca e indexa repositórios específicos ignorando filas (útil para corrigir contas individuais)label_sync -- reproduz streams de labels a partir do cursor 0 para recuperar negações perdidasplc_import -- importa em massa mapeamentos handle/DID do diretório PLCpalomar-sync -- sincroniza contagens de seguidores e PageRank para o OpenSearchServiço de upload de vídeo para usuários cujo PDS não suporta o video.bsky.app do Bluesky. Usa seu próprio DID (did:web:video.blacksky.community) para autenticar-se nos PDSes do usuário via JWTs de autenticação de serviço. Fluxo:
Labels de moderação vêm de serviços labeler (por exemplo, Ozone do Bluesky) via assinatura WebSocket. O ingester do Wintermute processa labels em uma fila dedicada label_live (baixo volume, separada do firehose principal). A ferramenta label_sync pode reproduzir o stream completo de um labeler para recuperar negações perdidas (remoções de labels) sem reinserir labels.
bskyO esquema bsky é criado pelas migrações do dataplane. Na primeira execução, o dataplane aplicará todas as migrações automaticamente. A única migração específica do Blacksky é 20260202T120000000Z-add-community-post.ts (tabela de postagens da comunidade). Se você não precisar de postagens da comunidade, pode removê-la.
O rsky-wintermute escreve no mesmo esquema. Todas as suas instruções INSERT usam ON CONFLICT, então é seguro executar wintermute e as migrações do dataplane em qualquer ordem.
pnpm install
pnpm build
node services/bsky/dataplane.js
| Variável | Obrigatório | Descrição |
|---|---|---|
DB_PRIMARY_URL | Sim | String de conexão do PostgreSQL com ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | Não | String de conexão da réplica de leitura |
BSKY_DATAPLANE_PORT | Não | Porta gRPC (padrão 2585) |
BSKY_REDIS_HOST | Não | Host:porta do Redis para cache (atualmente recomendado deixar desabilitado) |
BLACKSKY_MEMBERSHIP_DB_URL | Não | Banco de dados separado para adesão da comunidade (específico do Blacksky) |
node services/bsky/api.js
| Variável | Obrigatório | Descrição |
|---|---|---|
BSKY_APPVIEW_PORT | Não | Porta HTTP (padrão 2584) |
BSKY_DATAPLANE_URLS | Sim | URLs gRPC do dataplane separadas por vírgula |
BSKY_DID | Sim | DID da AppView (ex.: did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Sim | DID do serviço de moderação Ozone |
BSKY_ADMIN_PASSWORDS | Sim | Senhas de administrador separadas por vírgula para autenticação básica |
Um backfill de rede completa (todos ~42M de usuários, ~18,5B de registros) leva semanas mesmo com o processamento paralelo do wintermute. Espere:
Durante o backfill, a AppView é funcional, mas mostrará dados incompletos para usuários que ainda não foram backfilados. Eventos ao vivo são indexados imediatamente, independentemente do progresso do backfill.
Estes são problemas que encontramos ao inicializar uma AppView de rede completa. Se você estiver fazendo o mesmo, provavelmente encontrará alguns deles:
Corrupção de JSON no formato de texto COPY: O protocolo de texto COPY do PostgreSQL trata a barra invertida como caractere de escape. Se seu carregador em massa não escapar barras invertidas em strings JSON, \" se torna " e você obtém registros silenciosamente corrompidos. A coluna record.json é do tipo text (não jsonb), então o PostgreSQL não pegará isso. Encontramos ~66.000 registros corrompidos e tivemos que repará-los buscando novamente da API pública.
Bytes nulos em JSON: Alguns registros do AT Protocol contêm \u0000 (byte nulo), que é JSON válido de acordo com RFC 8259, mas rejeitado pelo JSON.parse() do Node.js. O dataplane retorna silenciosamente nulo para esses registros. Remova bytes nulos antes de escrever no banco de dados.
Sensibilidade ao formato de timestamp: O dataplane espera timestamps com precisão de milissegundos e sufixo Z (2026-01-12T19:45:23.307Z). Precisão de nanossegundos ou formato de fuso horário (+00:00) causa problemas sutis de ordenação e comparação.
Inchaço da tabela de notificações: Sem uma restrição única em (did, recordUri, reason), a tabela de notificações cresce ilimitadamente com duplicatas. A nossa chegou a 1,3 bilhão de linhas (663 GB) antes de percebermos. Adicionar ON CONFLICT DO NOTHING aos INSERTs só ajuda se o índice único existir primeiro, e criar o índice requer deduplicação dos dados existentes.
Tabelas de embed de postagens: As tabelas post_embed_image e post_embed_video não são populadas por padrão se seu indexador não as tratar. Sem elas, o filtro de mídia em getAuthorFeed não retorna nada. Elas precisam ser backfiladas separadamente.
Ordenação de negação de labels: Eventos de negação (remoção) de labels referenciam o label original por origem, URI e valor. Se as negações chegarem antes do label original (comum durante backfill), elas são silenciosamente descartadas. A ferramenta label_sync reproduz o stream completo para capturar essas.
Envenenamento da fila Fjall: O banco de dados Fjall incorporado (usado para as filas do wintermute) pode entrar em estado "envenenado" após falhas, bloqueando todas as operações de fila. A correção é excluir o diretório do banco de dados da fila e reiniciar – o wintermute recuperará a partir do cursor do relay (relays mantêm ~72 horas de histórico).
Inicialização do provedor TLS: O rustls requer a instalação explícita de um provedor criptográfico antes de qualquer conexão TLS. Sem rustls::crypto::aws_lc_rs::default_provider().install_default() na inicialização, a primeira conexão WebSocket com o firehose causará pânico.
Rotação de chave de assinatura após migração de conta: Quando os usuários migram entre PDSes, sua chave de assinatura muda. O dataplane armazena em cache dados de identidade com um staleTTL de 1 hora. Durante essa janela, a verificação JWT falha para usuários migrados. A correção é ignorar o cache na repetição da verificação e resolver diretamente do diretório PLC.
Baseado na execução de uma AppView de rede completa (todos ~42M de usuários, ~18,5B de registros).
| Recurso | Mínimo | Recomendado |
|---|---|---|
| CPU | 16 núcleos | 48+ núcleos |
| RAM | 64 GB | 256 GB |
| Armazenamento | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | Dedicado, mesma máquina ou baixa latência | Mesma máquina recomendada |
| Rede | Sustentado 100 Mbps | 1 Gbps+ |
Detalhamento de armazenamento (aproximado, rede completa):
| Grupo de tabelas | Tamanho |
|---|---|
| Postagens + registros | ~3,5 TB |
| Curtidas | ~2 TB |
| Seguindo | ~500 GB |
| Notificações | ~600 GB |
| Índices | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |
Para uma comunidade menor executando uma AppView parcial (indexando apenas membros da comunidade), os requisitos escalam aproximadamente linearmente com o número de contas indexadas.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
Conflitos tipicamente ocorrerão em packages/bsky/src/data-plane/server/routes/ e packages/bsky/src/api/. Resolva mantendo nossas adições juntamente com as alterações upstream.
Mesma que upstream: licenciado duplamente sob MIT e Apache 2.0. Veja LICENSE-MIT.txt e LICENSE-APACHE.txt.