AshAtproto
A full-stack AT Protocol SDK for the Ash Framework, built on top of atex. It allows you to authenticate users via ATProto OAuth, and interact with PDS (Personal Data Server) repositories as if they were standard Ash resources.
Features
- AshAuthentication Strategy: Enables the ATProto OAuth flow.
- XRPC OAuth Client: An HTTP client that handles session lifecycle, DPoP signatures, and automatic token refreshing.
- Decentralized Data Layer: Treat ATProto repositories as an Ash data layer. Supports creation, deletion, queries and cursor pagination.
-
Relationship Helpers: Resolve
strongReflinks and Constellation backlinks across PDS boundaries. - Igniter Code Generation Tasks: Automatically install auth boilerplate and scaffold Ash resources directly from Lexicon JSON files.
Installation
Add ash_atproto to your list of dependencies in mix.exs:
def deps do
[
{:ash_atproto, "~> 1.0.0"}
]
endAuthentication Setup
Rather than manually writing all the boilerplate required to store DIDs, handles, and OAuth tokens, AshAtproto provides an Igniter task to automatically configure your User resource.
Run the installer against your user resource:
mix ash_atproto.install MyApp.Accounts.User
This will safely inject the required attributes (:did, :handle, :oauth_tokens) and lifecycle actions (:register_with_atproto, :refresh) into your resource, and configure the AshAuthentication ATProto strategy block.
Example Configuration
Once installed, your AshAuthentication block will look something like this. (Using AshAuthentication.Secret is recommended for production values).
authentication do
strategies do
atproto do
registration_enabled? true
base_url MyApp.Secrets
private_key MyApp.Secrets
key_id MyApp.Secrets
# define the scopes your app needs
scopes ["repo:app.bsky.feed.post?action=create", "repo:app.bsky.feed.post?action=delete"]
end
end
endLocalhost OAuth Note
Due to the way the callback URL is parsed, if you're using OAuth in development, you should set this config in your dev.exs:
config :atex, Atex.OAuth, is_localhost: true
You must also use 127.0.0.1 instead of localhost in your URLs, as recommended by RFC8252.
The ATProto Data Layer
AshAtproto allows you to interact with the decentralized network using standard Ash actions. The Data Layer automatically handles JSON-to-Ash structuring, safe attribute filtering, and XRPC network calls.
Generating Resources from Lexicons
You can generate a fully-typed Ash Resource directly from an ATProto Lexicon JSON file using Igniter:
mix ash_atproto.gen.resource path/to/app.bsky.feed.post.json MyAppExample Resource
A resource uses a composite primary key (repo_did and rkey) and maps directly to a Lexicon collection.
defmodule MyApp.App.Bsky.Feed.Post do
use Ash.Resource, data_layer: AshAtproto.DataLayer
atproto_repo do
record_type "app.bsky.feed.post"
end
attributes do
# every atproto record needs these keys
attribute :repo_did, :string, primary_key?: true, allow_nil?: false
attribute :rkey, :string, primary_key?: true, allow_nil?: false
attribute :cid, :string
# lexicon specific attributes
attribute :text, :string, allow_nil?: false
attribute :createdAt, :utc_datetime, allow_nil?: false
end
actions do
defaults [:create, :destroy]
read :read do
primary? true
argument :repo_did, :string, allow_nil?: true
pagination do
required? true
keyset? true
end
prepare fn query, _ ->
repo_did = Ash.Query.get_argument(query, :repo_did)
repo_did =
repo_did ||
(query.context[:private][:actor] && query.context[:private][:actor].did)
if repo_did do
# sets the argument in the context, so data layer can access it
# the data layer also supports the `:uri` param instead
Ash.Query.set_context(query, %{repo_did: repo_did})
else
query
end
end
end
end
endUsage
- Reading Public Data (No Auth Required) Because ATProto data is public by default, you can query any user's repository without an OAuth token:
Ash.read!(MyApp.App.Bsky.Feed.Post, args: %{repo_did: "did:plc:user"})-
Writing Data (Auth Required)
Writes map to
com.atproto.repo.createRecordanddeleteRecord. You must pass the authenticated User as theactor. The Data Layer will automatically extract their OAuth tokens, generate DPoP signatures, and execute the XRPC request.
current_user = socket.assigns.current_user
Ash.create!(MyApp.App.Bsky.Feed.Post, %{text: "Hello from Elixir!"}, actor: current_user)XRPC OAuth Client
If you need to make arbitrary XRPC requests outside of the Data Layer, you can instantiate the OAuthClient directly from an authenticated Ash User resource. The access token is refreshed automatically as required.
user_resource = Ash.read_one!(MyApp.Accounts.User, authorize?: false)
{:ok, client} = AshAtproto.XRPC.OAuthClient.new(user_resource)
{:ok, response, client} =
Atex.XRPC.get(client, "com.atproto.repo.listRecords",
params: [repo: user_resource.handle, collection: "app.bsky.graph.follow"]
)Client Note
The client's state changes when a token is refreshed. Ensure you capture and reuse the updated client returned in the tuple.
For more details, including lexicon code generation and typed parameters, check the Atex.XRPC module.
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ash_atproto.