How to Make a PDS - Part 2

In part Part 1 I read the spec. Essential, but not very exciting. I decided to work on the authorization server next.

Authorization Server

Before we get into it, it's worth noting that the authorization server is potentially separate from the PDS itself. When it's separate you'll sometimes see it called an "entryway".

That's interesting, and possibly quite useful. In the context of our implementation, though, we're planning on using Rivet, which lets us split up our implementation into individual actors on the inside, almost like microservices. So having an actually separate server endpoint doesn't seem all that useful when the server implementation is distributed and scalable on the inside.

Anyway, that just means that we'll be implementing the authorization server as the same "server" as the PDS, but made up of several different "actors", just like everything else that we'll be implementing in the PDS.

Existing Libraries

The first step, I think, will be to see if we can find any existing libraries to do the OAuth stuff for us, preferably from the reference PDS implementation.

The reference PDS implementation can be found on GitHub here . It looks like @atproto/oauth-provider could be promising.

It's not easy to get any idea of the API surface from any public registry ( npmx docs didn't work for some reason ), so I'll go ahead and setup a dev env and install the package to explore with intellisense.

I made the first commit with just some super minimal rivet counter example, using pnpm for package manager, and tsx to run the TypeScript. You can clone and run that if you want.

Rivet HTTP

Now that I've done that it occurs to me that now would be a good time to figure out the details around hosting HTTP from rivet actors.

I think the idea is that we need to create a "gateway" service that will connect to our Rivet cluster and forward network requests to an actor that will handle them.

After a little experimenting I landed on a very simple start_gateway.ts:

import { createClient } from "rivetkit/client";
import { registry } from "./registry";
import { serve } from "@hono/node-server";
import { Hono } from "hono";

// Connect to Rivet
const client = createClient<typeof registry>();

// Get the http handler actor
const http = client.http.getOrCreate("main");

// Create a webserver
const app = new Hono();

// Forward all requests to the http handler
app.all("*", (c) => http.fetch(c.req.raw));

// Run the node.js webserver
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`);
});

And our rivet runner is implemented like this:

start_runner.ts:

import { registry } from "./registry.ts"
registry.startRunner();

registry.ts:

import { actor, setup } from "rivetkit";

// Create
export const http = actor({
onRequest(_c, req) {
return new Response(`Url: ${req.url}`);
},
});

export const registry = setup({
use: { http },
});

Now we shouldn't need to change the gateway at all from this point on, and we can just implement the request handling in the http actor.

Back To the Authentication Provider

Back to looking at libraries, it seems that the OAuthProvider class in @atproto/oauth-provider might actually implement pretty much all of the auth server logic for us.

It has bunch of options you pass in and requires that you pass in a store that implements the actual data storage it needs.

I'm wondering at this point whether I just make an actor for the auth provider and have it contain that class which basically does all the work for me, or if it's worth making every user account its own actor with it's own database, which is more scaleable. Or maybe those aren't mutually exclusive...

Anyway, to get things going, I put in erroring placholder implementations for all of the stores that the OAuthProvider needed, and managed to get the /.well-known/oauth-authorization-server endpoint working in this commit .

Then I proceeded with a bunch of API crawling and docs reading. Apparently there is a function in @atproto/oauth-provider that will setup Node.js / express middleware for hosting the auth server.

That could kind of mean that all I have to do is implement all the storage functions, that OAuthProvider needs, and then use the middleware. Auth server done!

The middleware unfortunately is not compatible with the more web-standard based Hono / Rivet style request handling, so I might have to rewrite that. But this is looking pretty interesting because I might not have to do much to get this going.

I feel like it could be useful to have more control over the auth server at some point, but if I could just flip a few switches and get the official auth server working almost unchanged, then I think that'd be a great start, and I could always replace more of it later, incrementally.

Takeaways So Far

Well that wraps up the week-end. I'm actually writing this on Monday morning, so it's time to get back to work. Here are my takeaways so far.

So far the ATProto reference implementation is pretty easy to read and surprisingly modular. It looks a lot easier than I thought it would be to re-use pieces of it.

We should hopefully be able to just use most of the ATProto auth server, and maybe it will be worth replacing more pieces of it later.

Rivet seems really thoughtfully designed, and it seems to have anticipated lots of the needs that I've run into in Roomy.

I still need to get more used to thinking in terms of actors and understanding how to put them together well.

I think this project is actually really great for me to get a more intimate understanding of ATProto and to practice using Rivet. 🚀

```