This post is about ligo.at, a decentralized and customizable "linktree-style" links page on top of atproto.


I expect that say ~95% of the website traffic will be first time visitors. Most people will be following a link in someones bio. And I want that first visit to load blazing fast. That's why I built ligo.at as an "old school", server rendered website. Nothing beats HTML sent directly from the server.

For that small percentage of visitors that come back to actually create their links page I provide login with their Atmosphere handle (usually a Bluesky account). And then I need to manage their sessions somehow.

The problem

Client rendered apps can store auth tokens client side. The user can decide anytime to delete those tokens, which makes the app unable to act in behalf of the user. Server rendered apps are somewhat different. They rely on a session cookie to keep track of the user session. What I learnt back in University is that this cookie is actually a key in your database with whatever relevant information you need. Even if the user deletes the cookie, this information will remain in the server's database.

And this works great for most websites. But in our case the information that I need to store are authentication tokens that let me update Bluesky profiles, publish posts or delete whatever I want. Oh no! I really don't what to be able to do that!

Some of this concerns may be solved with OAuth scopes. If I were able to only request write access to the at.ligo.* collections, I wouldn't have any power outside my website. Still, I don't want to store user tokens.

What I'm doing

So you might think that a server rendered website like ligo.at is evil because we will keep your tokens and post nasty stuff to your account without your permission. No! And here's how:

Instead of storing a key to a database, the session cookie actually contains the auth_token, refresh_token and all that. When the user tells me to update their links, I read the cookie, get the appropriate token, and send a request to their personal data server (PDS). As simple as that.

Currently I'm encoding the contents of the cookie (flask actually does this automatically). This has the added benefit that an attacker can't use ligo.at's cookie to access the user's tokens and use them to post stuff or whatever.

February 2026 update

As of 2026-02-11 and commit 7bbcc337, ligo.at uses atproto permissions so it's only allowed to modify at.ligo.* collections.