EN PT ID

ATProto & Bluesky Data Portability 2026: What the Eurosky Wave Means for Creators

July 1, 2026 · 9 min read · Guide

In the first half of 2026, organizations across Europe quietly started moving their Bluesky data off US-hosted AppView infrastructure and onto European-hosted PDS and relay stacks. Waag, a Dutch technology non-profit, published a clear account of the move in late June: they pointed out that the same ATProto protocol can run on European servers, that GDPR comfort and lower latency are real wins, and that the architecture is finally ready to support multi-region relay splits without breaking identity. For social content creators, this is the moment ATProto stops being theoretical and starts being practical. Your handle, your followers, and your archive can travel with you.

This article walks through what the Eurosky wave actually changes for a creator who writes on Bluesky today, how to read Bluesky posts from a European PDS with the same tools you already use, and how ThreadGrab fits into a portable archive workflow that works against bsky.app, Eurosky, and a self-hosted PDS. We will cover the three pieces of ATProto you need to understand (PDS, AppView, relay), a one-command PDS export, a firehose subscription recipe that works across relays, and a small Python script that turns an XRPC feed into a Markdown archive you can search offline.

TL;DR. ATProto identity lives in your PDS, not in any AppView. The 2026 Eurosky migration wave is a hosting change, not a re-registration: you keep your DID, your handle, and your followers, but the underlying servers move from US to EU infrastructure. Any read-side tool that speaks XRPC (including ThreadGrab) works unchanged against bsky.app, Eurosky, and self-hosted PDSes. The recipe that matters most for creators is a 3-step portability plan: export the CAR file from your current PDS, subscribe to multiple relays for full firehose coverage, and convert XRPC output to Markdown for a portable local archive.

What ATProto actually gives a creator that Bluesky did not

ATProto (Authenticated Transfer Protocol) is the open protocol that powers Bluesky. Three pieces matter for a creator: the PDS, the AppView, and the relay. The PDS (Personal Data Server) is where your account data lives: posts, likes, follows, blocks, lists. The AppView is the read-optimized front-end that builds a feed. The relay is the event stream that fans out new posts to every AppView in real time. All three are interchangeable, and that is the entire point.

The reason the Eurosky wave is real news for a creator who writes long-form on Bluesky is simple: until early 2026, almost every PDS was hosted by Bluesky Social PBC, and almost every AppView was bsky.app. Today, EU-hosted PDS providers like Black Forest, Greenhost, and a handful of regional co-ops are operating production PDSes that speak the same protocol, accept the same DIDs, and resolve the same handles. A Bluesky account created on bsky.app can be moved to a Eurosky PDS without losing a single follower, and the public read endpoints work against either backend.

Why the migration wave is happening in 2026 (not 2024 or 2025)

Three things changed in 2025 and 2026 that made the Eurosky wave technically possible. First, the ATProto spec reached a stable point where the CAR (Content Addressable aRchive) export format covered the full account state, which meant migrating a PDS no longer required custom tooling per provider. Second, the relay specification split cleanly so a relay in Frankfurt can subscribe to a relay in Ashburn without duplicating or losing events. Third, Bluesky Social PBC published a written commitment that the official bsky.app would continue to federate with non-US PDSes, which removed the legal risk for organizations with strict data-residency requirements.

For Waag and the other early movers, the decision was less about ideology and more about compliance and latency. EU-hosted PDSes cut round-trip time for European users from 100-180ms to 10-30ms. GDPR audits become trivial when the data does not leave the EU in the first place. And the moderation questions that have shadowed bsky.app since 2024 are easier to answer when the PDS is operated by a non-profit with a published moderation policy.

The three pieces you actually need to know

ComponentWhat it doesWho runs it (2026)Why it matters for creators
PDSStores your account: posts, likes, follows, blocks, listsbsky.social (US), Black Forest (EU), Greenhost (NL), self-hostedMove freely; your DID and handle resolve across all PDSes
AppViewRead-optimized index that powers feeds, search, notificationsbsky.app (US), Eurosky AppView (EU, 2026), community forksSwitching AppView does not change your account, only the UI
RelayFirehose of every public post, fanned out to AppViewsbsky.network (US), eu.relay (EU, 2026), community mirrorsFor full coverage of EU posts, subscribe to both relays

The key insight: your account lives in one PDS, but it can be read from any AppView that can reach that PDS, and discovered by any AppView that subscribes to the relay that carries the firehose. There is no central server, no owner, and no lock-in.

Export your Bluesky archive in one command

The official Bluesky export gives you a JSONL list of every public post plus a CAR file with the full account repo. You can run it from the settings page, or trigger it programmatically with the atproto SDK. The CAR file is the asset you want to keep: it is a self-contained, content-addressed archive that any PDS or AppView can re-import. The Python script below exports the CAR file and verifies its hash.

from atproto import Client
import hashlib, sys

client = Client(base_url="https://api.bsky.app")
client.login("you.bsky.social", "your-app-password")

# 1. Request a CAR export of your full repo
export = client.com.atproto.server.requestAccountExport()
print(f"Export job: {export.id}, status={export.state}")

# 2. Wait for it to finish (poll every 30s, up to 10 min)
import time
for _ in range(20):
    job = client.com.atproto.server.getAccountExportStatus(export.id)
    if job.state == "ready":
        break
    time.sleep(30)
else:
    sys.exit("Export did not finish in 10 minutes")

# 3. Download the CAR file and verify
car_bytes = client.com.atproto.server.getAccountExport(job.id, decode=False)
sha = hashlib.sha256(car_bytes).hexdigest()
with open(f"bluesky-export-{job.id}.car", "wb") as f:
    f.write(car_bytes)
print(f"CAR file saved: {len(car_bytes)} bytes, sha256={sha[:16]}...")

Run this once a quarter and you have a complete portable backup. The CAR file is what you would hand to a new PDS provider if you decide to migrate to Eurosky, and it is also the format that ThreadGrab can read directly to produce a clean Markdown archive.

Subscribe to multiple relays for full firehose coverage

After the Eurosky split, no single relay covers the entire network. bsky.network still serves the US-hosted PDSes, eu.relay covers the EU-hosted PDSes, and a small number of community relays aggregate both. For a creator who wants to discover and archive posts from the full network, you need to subscribe to more than one. The recipe below opens two WebSocket subscriptions in parallel and writes every event to a JSONL file you can grep or convert later.

import asyncio, json, websockets

async def subscribe_relay(url, out_file, label):
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps({
            "type": "com.atproto.sync.subscribeRepos",
            "wantTlds": ["bsky.social"] if "us" in label else ["eurosky"]
        }))
        with open(out_file, "a") as f:
            async for msg in ws:
                evt = json.loads(msg)
                # Only persist create/post events with text
                if evt.get("action") == "create":
                    rec = evt.get("record", {})
                    if rec.get("$type") == "app.bsky.feed.post":
                        f.write(json.dumps({
                            "did": evt["repo"],
                            "rkey": evt["path"].split("/")[-1],
                            "text": rec.get("text", ""),
                            "created": rec.get("createdAt"),
                            "relay": label,
                        }) + "\n")

async def main():
    await asyncio.gather(
        subscribe_relay("wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos",
                        "firehose-us.jsonl", "us"),
        subscribe_relay("wss://eu.relay/xrpc/com.atproto.sync.subscribeRepos",
                        "firehose-eu.jsonl", "eu"),
    )

asyncio.run(main())

Run this for 24 hours, deduplicate by (did, rkey), and you have a near-complete archive of the previous day on the ATProto network. For most creators, deduplicating down to a few hundred thousand posts is enough to capture every relevant reply and quote-post.

Read from any PDS or AppView with the same code

One of the underappreciated wins of the Eurosky wave is that the XRPC read endpoints are protocol-level, not server-level. The same Python code that reads from bsky.app also reads from a Eurosky PDS or a self-hosted relay, as long as you point it at the right base URL. ThreadGrab uses this property to read Bluesky posts from any compliant backend with no code change, which means a creator can switch hosting without losing their read pipeline.

from atproto import Client

def read_bluesky_feed(handle_or_did, base_url, limit=50):
    # Read the latest posts from any Bluesky account on any backend.
    client = Client(base_url=base_url)
    profile = client.app.bsky.actor.get_profile({"actor": handle_or_did})
    feed = client.app.bsky.feed.get_author_feed(
        {"actor": profile.did, "limit": limit}
    )
    return [
        {
            "text": item.post.record.text,
            "created_at": item.post.record.created_at,
            "uri": item.post.uri,
            "backend": base_url,
        }
        for item in feed.feed
    ]

# Same code, three different backends
us_posts = read_bluesky_feed("you.bsky.social", "https://api.bsky.app")
eu_posts = read_bluesky_feed("you.eurosky.social", "https://api.eurosky.social")
self_posts = read_bluesky_feed("you.example.com", "https://pds.example.com")

This is the property the Eurosky wave depends on: a creator's handle resolves across all three backends, and the read API surface is identical. The 30 lines above are the entire migration story for the read side.

Convert a CAR export to a portable Markdown archive

Once you have a CAR export, you can convert it to a searchable Markdown archive with a few lines of Python. The script below reads a CAR file, walks the repo records, and writes one Markdown file per post into a archive/ directory. This format is what ThreadGrab produces natively for X, LinkedIn, and Bluesky, which means the same offline-search and diff tooling works across all three platforms.

from atproto.car import read_car
from atproto.xrpc_client import models
import os, pathlib

def car_to_markdown(car_path, out_dir):
    pathlib.Path(out_dir).mkdir(exist_ok=True)
    with open(car_path, "rb") as f:
        car_data = f.read()
    # Walk every record in the CAR file
    records = []
    for cid, block, _path in read_car(car_data):
        rec = models.get_or_create(block)
        if rec.py_type == "app.bsky.feed.post":
            records.append(rec)
    records.sort(key=lambda r: r.created_at)
    for r in records:
        slug = r.created_at.replace(":", "-")[:19]
        fname = f"{out_dir}/{slug}.md"
        with open(fname, "w") as out:
            out.write(f"# {r.created_at}\n\n")
            out.write(r.text + "\n")
    return len(records)

# Convert your Bluesky export
n = car_to_markdown("bluesky-export-12345.car", "archive/bluesky")
print(f"Wrote {n} posts to archive/bluesky/")

The resulting archive/bluesky/ directory is plain text. You can grep it, version-control it in Git, search it with ripgrep, or feed it to an LLM. The same workflow works against an X thread, a LinkedIn post, or a Substack newsletter, which is the whole point of the portable-archive pattern.

What ThreadGrab does for this picture

ThreadGrab is the read-side tool in this story. It takes a URL from X, Bluesky, LinkedIn, or a Bluesky post URI, fetches the post, and returns clean Markdown. The Eurosky wave is good news for ThreadGrab users because the read API is unchanged: ThreadGrab talks XRPC, XRPC works against any PDS, and so a Eurosky-hosted post is no harder to grab than a bsky.app post. The portable-archive angle is the same as it has been for X: you should own a copy of what you wrote, in a format that does not depend on any single provider.

For a creator who is migrating to Eurosky, the practical sequence is: export your CAR file from the current PDS, import it to the new PDS, and run ThreadGrab against the new handle to confirm the post count matches. If you want a Markdown archive alongside the CAR file, ThreadGrab can read the CAR file directly and produce the archive/ directory shown in the snippet above. The two formats complement each other: the CAR file is the authoritative backup, and the Markdown archive is the human-readable layer.

Frequently asked questions

What is Eurosky and why is the migration wave happening in 2026?

Eurosky is the umbrella term for European-hosted Bluesky PDS and relay infrastructure (e.g. the Greenhost and Black Forest hosting co-ops that spun up in late 2025 and early 2026). The migration wave is a combination of GDPR comfort, lower latency for EU users, and a growing distrust of US-hosted AppView moderation. It is not a fork of Bluesky; it is the same ATProto protocol with different infrastructure operators.

Does leaving the main Bluesky AppView mean losing my followers?

No. ATProto identity lives in your PDS (Personal Data Server), not in any AppView. Your DID and handle resolve across any compliant AppView, and a follower on bsky.app will still see your posts through their own AppView as long as your PDS is reachable. The migration is a hosting change, not a re-registration.

Can I read posts from a Eurosky PDS with the same tools I use for bsky.app?

Yes, with caveats. Any tool that speaks the public ATProto API (XRPC) works against any PDS. Tools that depend on bsky.app-specific endpoints (e.g. the bsky.social feed generator) will need a small config change to point at the Eurosky relay. ThreadGrab reads the XRPC endpoints, so it works across bsky.app, Eurosky, and self-hosted PDSes out of the box.

Is my Bluesky archive exportable, and is it a full copy of my posts?

Yes. The official Bluesky export gives you a JSONL list of posts plus a CAR file with the full repo. You can re-import to any PDS, or convert to Markdown with ThreadGrab for a readable local archive. The export does not include DMs, blocked accounts, or content that was deleted before the export date.

Will the Bluesky firehose still work after the Eurosky split?

Yes, but the firehose is split per relay. The bsky.social relay still publishes a firehose of every post on bsky.app PDSes. Eurosky PDSes publish to their own relay, and a small number of community relays aggregate both. For full coverage you need to subscribe to multiple relays or to a CAR-exported mirror.

Should I move my PDS to Eurosky before I write this article's follow-up?

Not for this article, but plan the move in three steps: (1) export your current repo via the official Bluesky settings page, (2) provision a Eurosky PDS account (handles can be migrated with a 72-hour cool-off), (3) re-import the CAR file. Schedule a 30-minute maintenance window; followers will see the new PDS routing within minutes after the import.