# AshAtproto
A full-stack AT Protocol SDK for the [Ash Framework](https://ash-hq.org/), built on top of [atex](https://tangled.org/comet.sh/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 `strongRef` links 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`:
```elixir
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:
```bash
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](https://hexdocs.pm/ash_authentication/AshAuthentication.Secret.html) is recommended for production values).
```elixir
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`:
```elixir
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](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3).
## 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:
```bash
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.
```elixir
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
- Reading Public Data (No Auth Required)
Because ATProto data is public by default, you can query any user's repository without an OAuth token:
```elixir
Ash.read!(MyApp.App.Bsky.Feed.Post, args: %{repo_did: "did:plc:user"})
```
- Writing Data (Auth Required)
Writes map to `com.atproto.repo.createRecord` and `deleteRecord`. You must pass the authenticated User as the `actor`. The Data Layer will automatically extract their OAuth tokens, generate DPoP signatures, and execute the XRPC request.
```elixir
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.
```elixir
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](https://hexdocs.pm/atex/Atex.XRPC.html) module.
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and published on [HexDocs](https://hexdocs.pm). Once published, the docs can be found at <https://hexdocs.pm/ash_atproto>.