Zum Inhalt springen

Föderation mit Bluesky (Relay)

Gilt für prod. Der Anschluss ist dort vorgesehen, der Zeitpunkt offen (ROADMAP Stufe R). Der Test-PDS ist absichtlich nicht am Relay, höchstens temporär (PDS_CRAWLERS nicht gesetzt, ATVOTO_RELAY_FEDERATION=false), und darf zurückgesetzt werden. Was über Bluesky läuft (Crosspost, Poller, Feed): INTEROPERABILITY.md. Erfahrungen aus poltr (Throttling von pds.poltr.info, Umzug auf pds2.poltr.info) sind hier eingeflossen.

Damit Konten von atvoto auf bsky.app sichtbar sind, abonniert der Relay (bsky.network) den Firehose unseres PDS (com.atproto.sync.subscribeRepos) und reicht #commit, #identity und #account an die Bluesky-AppView weiter.

PDS (pds.atvoto.ch) ──WebSocket──► Relay (bsky.network) ──► Bluesky-AppView (bsky.app)

Entdeckt wird der PDS über PDS_CRAWLERS=https://bsky.network (Meldung beim Start) oder com.atproto.sync.requestCrawl. Der Relay löst die Server-DID über plc.directory auf und verbindet sich.


  1. pds-data nie löschen, den PDS nie zurücksetzen.
  2. Server-DID und Rotation-Key nie rotieren. Der Rotation-Key gehört zusätzlich in den Passwort-Manager.
  3. Kein Restore ohne die Reset-Prozedur (unten): Ein Restore ist für den Relay ein Erststart.
  4. Den endgültigen Hostnamen nie im Test verwenden. Throttling hängt am Hostnamen und ist praktisch nicht rückgängig zu machen.
  5. Vor 100 Konten Bluesky um ein höheres Limit bitten (unten). Community-Konten zählen mit.

  1. Server-DID als did:plc. Der Bluesky-PDS liefert kein /.well-known/did.json, did:web funktioniert für die Föderation deshalb nicht (in test steht did:web:pds.test.atvoto.ch, dort egal). Die DID wird aus dem Rotation-Key abgeleitet und bei plc.directory registriert: Genesis-Operation mit rotationKeys und verificationMethods.atproto = did:key des Keys, alsoKnownAs: ["at://pds.atvoto.ch"], Dienst atproto_pds → https://pds.atvoto.ch, signiert über DAG-CBOR. scripts/testenv_pds_reset.py (Modus 1) erzeugt Key und DID und registriert sie; trotz Name ist dieser Teil allgemein, vor prod durchsehen. Weicht die von plc.directory berechnete DID ab, gilt die aus deren Antwort.
  2. PDS konfigurieren: PDS_SERVICE_DID (so heisst die Variable), PDS_CRAWLERS=https://bsky.network, PDS_HOSTNAME=pds.atvoto.ch. Prüfen: curl https://pds.atvoto.ch/xrpc/com.atproto.server.describeServer | jq .did.
  3. Ingress für WebSockets: Der Relay hält eine dauerhafte Verbindung. Zu kurze Read-/Idle-Timeouts am Ingress (Traefik) trennen sie (ERR_STREAM_PREMATURE_CLOSE im PDS-Log). In prod prüfen und ggf. Timeouts für den PDS-Host hochsetzen (poltr/nginx brauchte 3600 s).
  4. Sichere Boot-Reihenfolge einhalten (unten), dann requestCrawl:
    Terminal-Fenster
    curl -X POST https://bsky.network/xrpc/com.atproto.sync.requestCrawl \
    -H 'Content-Type: application/json' -d '{"hostname": "pds.atvoto.ch"}'
  5. Registrar umstellen: ATVOTO_RELAY_FEDERATION=true (siehe „Neue Konten“).
  6. Prüfen: PDS-Log zeigt den WebSocket-Upgrade des Relays (101); getRepoStatus eines Kontos ist active; das Profil erscheint auf bsky.app.

Der PDS sendet beim Anlegen ein #identity-Event und den Commit mit dem Profil. Verarbeitet die Bluesky-AppView das #identity vor dem Commit, legt sie einen kaputten Stub an (createdAt: 0001-01-01…), der sich von unserer Seite nicht mehr reparieren lässt (Handle-Toggle, Profil-Update, Posts, requestCrawl helfen nicht).

atvoto umgeht das im Registrar (relay_sync in services/writer/src/registrar/pds_admin.py, nur mit ATVOTO_RELAY_FEDERATION=true), bei createAccount und createDossier:

1. Konto auf dem PDS anlegen
2. warten, bis die DID auf plc.directory auflösbar ist
3. Profil schreiben (app.bsky.actor.profile/self)
4. requestCrawl an den Relay
5. warten, bis der Relay den Commit hat (getLatestCommit = 200)
6. Handle kurz umschalten → neues #identity-Event, jetzt mit Repo-Daten

Alle Schritte nach 3 sind nicht fatal: Das Konto entsteht auch, wenn der Relay nicht antwortet.


Nicht vollständig dokumentiert; Quelle ist der Relay-Code (indigo cmd/relay).

Konten pro PDS: 100. Ab Konto 101 setzt der Relay neue Konten auf host-throttled und verwirft deren Events stillschweigend. requestCrawl gelingt weiter, der PDS merkt nichts. Höheres Limit nur über das Bluesky-Team (PDS-Admins-Discord): Projekt, erwartete Kontenzahl, Moderation, Missbrauchsplan, Betreiber angeben. Wird das Limit erhöht, werden gedrosselte Konten automatisch wieder aktiv. → Ab etwa 80 Konten anfragen; Kontenzahl überwachen (goat pds account list --pds-host … | wc -l).

Event-Raten pro PDS: etwa 50/s, 1500/h, 10 000/Tag, Konto-Erstellung 5/s (skaliert mit dem Konto-Limit). Bei Überschreitung wartet der Relay; Events werden verzögert, nicht verworfen. Die Zähler leben nur im Speicher des Relays.

Status: Host active/idle/offline/throttled/banned; Konto active/throttled/host-throttled. Von aussen erscheint host-throttled als throttled, die beiden sind nicht unterscheidbar.

Praktisch heisst das: ATProto ist offen, die Sichtbarkeit in Bluesky hängt aber am Relay von Bluesky. Föderation mit bsky.network ist faktisch bewilligungspflichtig.


Alle drei treten nur bei (Neu-)Initialisierung oder Restore auf, nicht im laufenden Betrieb.

Sendet der PDS #identity, bevor das Repo beim Relay ingestiert werden kann, stuft der Relay es als verdächtig ein: RepoInactive: throttled, dauerhaft. Nur ein Neuaufbau des Kontos hilft; trifft es den Hostnamen, nur ein neuer Hostname.

Sichere Boot-Reihenfolge:

1. PDS starten
2. warten, bis Firehose und Relay-Handshake stehen
3. Repo anlegen, ersten Commit schreiben
4. prüfen: Relay getLatestCommit OK
5. erst dann #identity senden

Nach einem Reset beginnt seq wieder bei 1, der Relay verbindet sich aber mit seinem letzten Cursor (z.B. 2200) und ignoriert alles darunter. Symptom: Der PDS läuft, im Netz kommt nichts an. Lösung: PDS stoppen, in sequencer.sqlite (repo_seq) die Sequenz über den alten Wert setzen (Schema vorher prüfen; sqlite_sequence allein setzt der PDS beim Start zurück), PDS starten, echte Aktivität erzeugen (z.B. Profil-Update), requestCrawl. Lücken in seq sind erlaubt, die Folge muss nur steigen.

Technisch ein Erststart, also dasselbe Throttling-Risiko. Identity-Events kommen zuletzt: PDS ohne Identity-Sync starten, Repos einspielen, Relay-Verbindung stabilisieren, warten bis getLatestCommit dem aktuellen Stand entspricht, erst dann Identity-Events zulassen. Notfall, wenn es trotzdem passiert: Konto löschen, PLC-Eintrag rotieren, Repo neu importieren, sauber neu ankündigen.


Terminal-Fenster
# Konto beim Relay
curl "https://bsky.network/xrpc/com.atproto.sync.getRepoStatus?did=<did>" # active / throttled / RepoNotFound
curl "https://bsky.network/xrpc/com.atproto.sync.getLatestCommit?did=<did>" # Relay hat den Commit
curl "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=<did>"
# PDS
curl https://pds.atvoto.ch/xrpc/com.atproto.server.describeServer | jq .did
curl https://plc.directory/<server-did>
Symptom Ursache, Massnahme
RepoNotFound für alle Konten Server-DID falsch oder nicht bei plc.directory; Relay hat nie gecrawlt (requestCrawl); WebSocket-Timeouts. Neue PDS brauchen 30–120 min bis zum Abo
throttled für einzelne Konten Konto-Limit erreicht oder Spam-Verdacht → Bluesky-Team
throttled für alle, auch Konto 1 Hostname markiert → Bluesky-Team oder neuer Hostname (alle Konten gehen verloren)
ERR_STREAM_PREMATURE_CLOSE Ingress-Timeout für WebSockets
Events kommen nicht an nach Reset Sequenz-Lücke (oben)

Was nur Bluesky kann: Konto-Limit erhöhen (/admin/pds/changeLimits), Host oder Domain entsperren, erzwungener Crawl, Takedowns. Kontakt über den PDS-Admins-Discord, ohne SLA.


Variable Wo Zweck
PDS_SERVICE_DID PDS did:plc des Servers
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX pds-secrets signiert PLC-Operationen
PDS_CRAWLERS PDS Relay, der beim Start benachrichtigt wird (nur prod)
PDS_HOSTNAME PDS, Registrar öffentlicher Hostname
ATVOTO_RELAY_FEDERATION Registrar relay_sync bei neuen Konten (nur prod true)
BSKY_RELAY_URL Registrar, Skript Standard https://bsky.network