if you're building on atproto, you've probably reached for "sign in with bluesky" as your auth story. it works! the OAuth flow is fine. but every time a user shows up without a bluesky account, you have to send them to bluesky first, watch them get confused by a totally different brand, and hope they come back. that's a weird seam in your own product.

i hit this with pollen and decided to just run my own PDS. lol. now pollen.place/register creates an atmosphere account directly. the PDS lives at pds.pollen.place, runs in the background, and nobody touches it but the server. users never even see the URL.

here's how it works and why i think more atmosphere apps should do this.

the setup is small

a PDS (personal data server) is the thing that stores a user's atproto repo. their posts, follows, profile, signing keys. bluesky runs PDSes for their users at *.host.bsky.network. you can run one too.

i'm running tranquil, which is a rust implementation of the PDS spec. one binary, talks postgres, serves the standard xrpc endpoints. it deploys to fly.io as a single container:

# pds/fly.toml
app = 'pollen-pds'
primary_region = 'sjc'

[http_service]
  internal_port = 3000
  force_https = true
  min_machines_running = 1

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

one shared-cpu machine, 1gb of ram. it costs me single-digit dollars a month right now. it'll scale up when there's a reason to scale it up. the atproto pds spec tells you exactly what endpoints to expose, and tranquil exposes them all, so it participates in the network like any other PDS — jetstream sees it, plc.directory knows about it, other apps can read records from it.

/register and /login are just thin wrappers

the PDS has its own API for creating accounts and signing in. it's com.atproto.server.createAccount and com.atproto.server.createSession. nothing stops you from calling those yourself from your own backend.

so pollen's registration route is just a server endpoint that does the boring stuff (cloudflare turnstile, rate limit, validation) and then forwards that to the PDS:

// src/server/routes/register.ts
const accountRes = await fetch(`${PDS_URL}/xrpc/com.atproto.server.createAccount`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    handle: handle + ".pollen.place",
    email,
    password,
    inviteCode,
  }),
});

that's the whole integration! the PDS returns a DID, an access token, and the account is ✨ real ✨ on the network. then i can use the access token to write the user's profile record (place.pollen.actor.profile) so they have a display name and avatar from the very first second they exist.

login is the same idea: the form on pollen.place/login posts to my server, my server calls createSession against the PDS, i take the session and store it. from the user's perspective they signed up for pollen, with a pollen.place handle, and that's all they know.

users don't need to know what a PDS is

this is the part i care about. atproto is a great protocol but "personal data server" is a phrase only nerds want to think about. asking new users to pick one, or to understand that their account "lives somewhere else," is a tax on people who just want to make a blog.

with a PDS sitting behind your own register/login routes, the protocol stays the protocol but the product gets to be the product. you can write a clean welcome page that says "make an account on pollen" instead of "select your atproto provider." your handle suffix becomes a branding decision (yourname.pollen.place) instead of an implementation detail. your support emails don't have to explain what bluesky has to do with anything.

and the openness is still there, the account is a real atproto account. it works in any atmosphere app. it can be migrated out to another PDS whenever the user wants. nothing about wrapping the PDS makes it less portable. you're just hiding the wiring.

the back door

one thing to know: tranquil ships with its own little web frontend at /app/* — signup, login, account management. if you leave that open, people can create accounts on your PDS without going through your register flow at all. that's not what you want, because your register flow is where you do captcha, rate limiting, profile setup, onboarding.

an nginx redirect handles it:

location = /app/signup {
    return 301 https://pollen.place/register;
}

now pds.pollen.place/app/signup lands on pollen's branded registration page instead of tranquil's form. the api endpoints (the things actual apps and migrations need) keep working untouched. only the human-facing pages get redirected.

if you're building an atmosphere app, do this too!

i think this is the move for any atmosphere app that wants its own identity. the cost is small (a single fly machine, an afternoon of setup), the upside is real (your users have real atproto accounts with your brand), and the surface area you're maintaining is almost zero because the protocol does the heavy lifting.

a non-exhaustive list of why running your own PDS is good:

  • your users get handles in your namespace (keith.yourapp.com) instead of keith.bsky.social

  • your sign-up funnel doesn't bounce through bluesky

  • the accounts are still portable. anyone can leave with pdsmoover and keep their DID

  • you participate in the network the same way bluesky does

the only thing you're really taking on is "is the PDS up?" and tranquil's been pretty boring in the best way.

a PDS as a service?

okay, last thought, because not everyone wants to run a fly machine.

most of the friction around running a PDS today isn't the running — tranquil basically just runs — it's the surrounding stuff. you need a domain. you need email sending configured for password resets and verification. email is still such an annoying piece of all of this. you need handle validation, invite code generation, a small admin tool. you need to know not to expose /app/signup. you need to keep up with spec changes. none of this is hard, but it's a checklist, and a checklist is enough to stop most people from doing it.

there's an obvious product hiding here: PDS as a service for atmosphere apps. you bring a domain. someone else runs the PDS, gives you the credentials, and exposes a tiny API: createAccount, createSession, resetPassword. you keep all the user-facing flows on your own site. they keep the boring infra healthy. you pay them a few dollars a user per year. they make it possible for someone to launch an atmosphere app over a weekend without ever writing a Dockerfile.

if anyone's building this, i'd love to hear about it!

links