Fork de la implementación de referencia del Protocolo AT con AppView optimizado para rendimiento, indexador de firehose basado en Rust, almacenamiento en caché Redis y características comunitarias para redes sociales autoalojadas a escala.
Esta es una bifurcación (fork) de Blacksky de la implementación de referencia del AT Protocol de Bluesky Social PBC. Impulsa el AppView en api.blacksky.community.
Publicamos esto por transparencia y para que otras comunidades puedan beneficiarse del trabajo. Este repositorio no acepta contribuciones, issues ni PRs. Si desea la implementación canónica de atproto, use bluesky-social/atproto.
Todos los cambios están en packages/bsky (lógica de AppView), services/bsky (configuración de ejecución) y una migración personalizada. Todo lo demás es upstream.
El dataplane upstream incluye un consumidor de firehose TypeScript (subscription.ts) que indexa eventos directamente. Lo reemplazamos con , un indexador Rust, por varias razones:
El dataplane y el AppView de este repositorio siguen funcionando tal cual. Leen de la base de datos PostgreSQL que escribe wintermute. Simplemente no iniciamos la suscripción de firehose integrada.
Estas son ampliamente útiles para cualquiera que aloje su propio AppView a escala.
Optimización de consultas LATERAL JOIN (packages/bsky/src/data-plane/server/routes/feeds.ts)
getTimeline y getListFeed reescritos con LATERAL JOIN de PostgreSQL para forzar el uso de índices por usuario en lugar de escaneos completos de tabla. Mejora importante para usuarios que siguen a miles de cuentas.Capa de caché Redis (packages/bsky/src/data-plane/server/cache/)
Timestamp pierden su método .toDate() después de un round-trip JSON a través de Redis, causando una hidratación de perfil incompleta en aciertos de caché. Actualmente ejecutamos con la caché Redis deshabilitada. La solución es serializar los timestamps como cadenas ISO al escribir en caché y reconstruirlos al leer.Aplicación del lado del servidor de preferencias de notificaciones (packages/bsky/src/api/app/bsky/notification/listNotifications.ts)
reasons, el servidor aplica las preferencias de notificaciones guardadas del usuario. Sin esto, las preferencias solo se aplican del lado del cliente y no tienen efecto.Corrección de clave de firma obsoleta en verificador de autenticación (packages/bsky/src/auth-verifier.ts)
forceRefresh), omite la caché de identidad en memoria del dataplane y resuelve el documento DID directamente desde el directorio PLC. Soluciona fallos de autenticación después de la migración de cuentas donde la clave de firma rota pero la caché mantiene la clave antigua.Sanitización JSON (packages/bsky/src/data-plane/server/routes/records.ts)
\u0000) y caracteres de control de los registros almacenados antes del análisis JSON. Estos son válidos según RFC 8259 pero rechazados por JSON.parse() de Node.js, lo que provoca fallos silenciosos de rowToRecord en el dataplane que se manifiestan como publicaciones faltantes.Infraestructura para publicaciones comunitarias privadas que residen en el AppView en lugar de PDS individuales. Específico de cómo funciona Blacksky, pero podría servir como referencia para otras comunidades.
community.blacksky.feed.* con endpoints para enviar, obtener, eliminar, timeline y vistas de hiloscommunity_post (migración: 20260202T120000000Z-add-community-post.ts)getPostThreadV2 para hilos mixtos de publicaciones estándar y comunitariasBLACKSKY_MEMBERSHIP_DB_URL)Bluesky Relay (bsky.network)
|
v
rsky-wintermute -----> PostgreSQL 17 <----- Palomar
(indexador Rust) | (búsqueda Go)
- consumidor firehose | |
- reindexador | v
- indexador de etiquetas | OpenSearch
- indexador directo |
v
bsky-dataplane (gRPC :2585) <--- Redis (opcional)
|
v
bsky-appview (HTTP :2584)
|
v
Proxy inverso (Caddy/nginx)
| Componente | Fuente | Propósito |
|---|---|---|
| rsky-wintermute | blacksky-algorithms/rsky | Indexador de firehose en Rust: consume eventos, reindexa repositorios, indexa registros en PostgreSQL |
| rsky-relay | blacksky-algorithms/rsky | Relay del AT Protocol para recibir etiquetas de moderación de servicios etiquetadores |
| rsky-video | blacksky-algorithms/rsky | Servicio de subida de vídeo: transcode vía Bunny Stream CDN, sube referencias de blobs a los PDS de los usuarios |
| bsky-dataplane | Este repositorio (services/bsky) | Capa de datos gRPC sobre PostgreSQL |
| bsky-appview | Este repositorio (services/bsky) | Servidor API HTTP para endpoints XRPC de app.bsky.* |
| Palomar | blacksky-algorithms/indigo | Búsqueda de texto completo: indexa perfiles y publicaciones en OpenSearch con potenciación por número de seguidores |
| palomar-sync | blacksky-algorithms/rsky | Sincroniza conteos de seguidores y puntuaciones PageRank desde PostgreSQL a OpenSearch |
Wintermute es un servicio monolítico en Rust con cuatro rutas de procesamiento paralelo:
bsky.network vía WebSocket, escribe eventos en colas Fjall (almacén clave-valor incrustado)ON CONFLICT para idempotenciaHerramientas CLI adicionales incluidas en el repositorio rsky:
queue_backfill -- pone en cola DIDs para reindexación desde CSV, descubrimiento de PDS o listas directas de DIDsdirect_index -- obtiene e indexa repositorios específicos sin pasar por colas (útil para reparar cuentas individuales)label_sync -- reproduce flujos de etiquetas desde el cursor 0 para ponerse al día con negaciones perdidasplc_import -- importa asignaciones de handles/DIDs desde el directorio PLCpalomar-sync -- sincroniza conteos de seguidores y PageRank a OpenSearchServicio de subida de vídeo para usuarios cuyo PDS no soporta video.bsky.app de Bluesky. Usa su propio DID (did:web:video.blacksky.community) para autenticarse en los PDS de los usuarios mediante JWTs de autenticación de servicio. Flujo:
Las etiquetas de moderación provienen de servicios etiquetadores (ej., Ozone de Bluesky) mediante suscripción WebSocket. El ingester de Wintermute procesa las etiquetas en una cola dedicada label_live (bajo volumen, separada del firehose principal). La herramienta label_sync puede reproducir el flujo completo de un etiquetador para ponerse al día con negaciones perdidas (eliminaciones de etiquetas) sin reinsertar etiquetas.
bskyEl esquema bsky es creado por las migraciones del dataplane. En la primera ejecución, el dataplane aplicará todas las migraciones automáticamente. La única migración específica de Blacksky es 20260202T120000000Z-add-community-post.ts (tabla de publicaciones comunitarias). Si no necesita publicaciones comunitarias, puede eliminarla.
rsky-wintermute escribe en el mismo esquema. Todas sus instrucciones INSERT usan ON CONFLICT, por lo que es seguro ejecutar wintermute y las migraciones del dataplane en cualquier orden.
pnpm install
pnpm build
node services/bsky/dataplane.js
| Variable | Requerida | Descripción |
|---|---|---|
DB_PRIMARY_URL | Sí | Cadena de conexión PostgreSQL con ?options=-csearch_path%3Dbsky |
DB_REPLICA_URL | No | Cadena de conexión de réplica de lectura |
BSKY_DATAPLANE_PORT | No | Puerto gRPC (por defecto 2585) |
BSKY_REDIS_HOST | No | Host:puerto de Redis para caché (actualmente se recomienda dejarlo deshabilitado) |
BLACKSKY_MEMBERSHIP_DB_URL | No | BD separada para membresía comunitaria (específico de Blacksky) |
node services/bsky/api.js
| Variable | Requerida | Descripción |
|---|---|---|
BSKY_APPVIEW_PORT | No | Puerto HTTP (por defecto 2584) |
BSKY_DATAPLANE_URLS | Sí | URLs gRPC del dataplane separadas por comas |
BSKY_DID | Sí | DID del AppView (ej. did:web:api.example.com) |
BSKY_MOD_SERVICE_DID | Sí | DID del servicio de moderación Ozone |
BSKY_ADMIN_PASSWORDS | Sí | Contraseñas de administrador separadas por comas para autenticación básica |
Una reindexación completa de la red (todos los ~42M de usuarios, ~18.5B de registros) toma semanas incluso con el procesamiento paralelo de wintermute. Espere:
Durante la reindexación, el AppView es funcional pero mostrará datos incompletos para los usuarios que aún no han sido reindexados. Los eventos en vivo se indexan inmediatamente independientemente del progreso de la reindexación.
Estos son problemas que encontramos al iniciar un AppView de red completa. Si está haciendo lo mismo, probablemente encontrará algunos de estos:
Corrupción JSON en formato de texto COPY: El protocolo de texto COPY de PostgreSQL trata la barra invertida como un carácter de escape. Si su cargador masivo no escapa las barras invertidas en cadenas JSON, \" se convierte en " y obtiene registros corruptos silenciosamente. La columna record.json es de tipo text (no jsonb), por lo que PostgreSQL no lo detectará. Encontramos ~66,000 registros corruptos y tuvimos que repararlos volviendo a obtenerlos de la API pública.
Bytes nulos en JSON: Algunos registros del AT Protocol contienen \u0000 (byte nulo), que es JSON válido según RFC 8259 pero rechazado por JSON.parse() de Node.js. El dataplane devuelve silenciosamente null para estos registros. Elimine los bytes nulos antes de escribir en la base de datos.
Sensibilidad al formato de timestamp: El dataplane espera timestamps con precisión de milisegundos y sufijo Z (2026-01-12T19:45:23.307Z). La precisión de nanosegundos o el formato de desplazamiento de zona horaria (+00:00) causan problemas sutiles de ordenación y comparación.
Inflado de la tabla de notificaciones: Sin una restricción única en (did, recordUri, reason), la tabla de notificaciones crece sin límite con duplicados. La nuestra alcanzó 1.3 mil millones de filas (663 GB) antes de que lo detectáramos. Agregar ON CONFLICT DO NOTHING a los INSERTs solo ayuda si el índice único existe primero, y crear el índice requiere deduplicación de los datos existentes.
Tablas de incrustaciones de publicaciones: Las tablas post_embed_image y post_embed_video no se pueblan por defecto si su indexador no las maneja. Sin ellas, el filtro de medios en getAuthorFeed no devuelve nada. Estas deben reindexarse por separado.
Orden de negación de etiquetas: Los eventos de negación de etiquetas (eliminación) hacen referencia a la etiqueta original por fuente, URI y valor. Si las negaciones llegan antes que la etiqueta original (común durante la reindexación), se descartan silenciosamente. La herramienta label_sync reproduce el flujo completo para detectarlas.
Envenenamiento de colas Fjall: La base de datos incrustada Fjall (utilizada para las colas de wintermute) puede entrar en un estado "envenenado" después de fallos, bloqueando todas las operaciones de cola. La solución es eliminar el directorio de la base de datos de colas y reiniciar; wintermute se pondrá al día desde el cursor del relay (los relays mantienen ~72 horas de historial).
Inicialización del proveedor TLS: rustls de Rust requiere instalar explícitamente un proveedor criptográfico antes de cualquier conexión TLS. Sin rustls::crypto::aws_lc_rs::default_provider().install_default() al inicio, la primera conexión WebSocket al firehose entra en pánico.
Rotación de clave de firma después de migración de cuenta: Cuando los usuarios migran entre PDS, su clave de firma cambia. El dataplane almacena en caché los datos de identidad con un staleTTL de 1 hora. Durante esa ventana, la verificación JWT falla para los usuarios migrados. La solución es omitir la caché en el reintento de verificación y resolver directamente desde el directorio PLC.
Basado en la ejecución de un AppView de red completa (todos los ~42M de usuarios, ~18.5B de registros).
| Recurso | Mínimo | Recomendado |
|---|---|---|
| CPU | 16 núcleos | 48+ núcleos |
| RAM | 64 GB | 256 GB |
| Almacenamiento | 10 TB NVMe | 28+ TB NVMe (RAID) |
| PostgreSQL | Dedicado, misma máquina o baja latencia | Se recomienda misma máquina |
| Red | 100 Mbps sostenidos | 1 Gbps+ |
Desglose de almacenamiento (aproximado, red completa):
| Grupo de tablas | Tamaño |
|---|---|
| Publicaciones + registros | ~3.5 TB |
| Me gusta | ~2 TB |
| Seguidores | ~500 GB |
| Notificaciones | ~600 GB |
| Índices | ~4 TB |
| OpenSearch (Palomar) | ~500 GB |
Para una comunidad más pequeña que ejecuta un AppView parcial (indexando solo miembros de la comunidad), los requisitos escalan aproximadamente de forma lineal con las cuentas indexadas.
git remote add upstream https://github.com/bluesky-social/atproto.git
git fetch upstream
git merge upstream/main
Los conflictos normalmente estarán en packages/bsky/src/data-plane/server/routes/ y packages/bsky/src/api/. Resuélvalos manteniendo nuestras adiciones junto con los cambios upstream.
Igual que upstream: licencia dual bajo MIT y Apache 2.0. Ver LICENSE-MIT.txt y LICENSE-APACHE.txt.