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

Installation

Add ash_atproto to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_atproto, "~> 1.0.0"}
  ]
end

Authentication 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
  end

Localhost 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 MyApp

Example 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
end

Usage

Ash.read!(MyApp.App.Bsky.Feed.Post, args: %{repo_did: "did:plc:user"})
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.