In this first article, we'll learn the basics of how to publish to Bluesky using Elixir and the Req library. In the follow-up articles, we'll gradually add more features like tags, mentions, links and website cards.

For now, we'll keep it simple and start with posting a plain text message.

Prerequisites

Before you start, we need a couple of things.

Ensure the Req HTTP client is installed in your project (see here for instructions).

You'll need the hostname of Bluesky entryway which is usually https://bsky.social (see here for more information). You'll also need your Bluesky username, in my case yellowduck.be (your handle without the @ symbol) and your Bluesky password.

Terminology

You'll see some terminology in the examples which you might not be familiar with. Here's a short overview:

  • PDS: Personal Data Server. A PDS acts as the participant’s agent in the network. This is what hosts your data (like the posts you’ve created) in your repository. It also handles your account & login, manages your repo’s signing key, stores any of your private data (like which accounts you have muted), and handles the services you talk to for any request. This is explained here.

  • DID: Decentralized Identifiers (DIDs) which are used as persistent, long-term account identifiers. DID is a W3C standard, with many standardized and proposed DID method implementations. You'll find more information about this here.

  • AT Protocol: the protocol on which Bluesky is built. The AT Protocol is an open, decentralized network for building social applications.

  • Graphemes: the visual "atom" of text -- what we think of as a "character". Graphemes are made of multiple code-points.

Step 1: Create an app password for your account

The first step we need to take is to create an app password for your account. You could use your account password as well, but that has some security risks when someone get a hold of your password.

App passwords have most of the same abilities as the user's account password, however they're restricted from destructive actions such as account deletion or account migration. They are also restricted from creating additional app passwords. App passwords are of the form xxxx-xxxx-xxxx-xxxx (as explained here)

You can create one by going to Settings > Privacy and Security > App Passwords.

Step 2: logging in (creating a session)

The first step in the process is to login and create a session. This is done by sending a HTTP POST request to the com.atproto.server.createSession endpoint.

It takes a JSON body with your username as the identifier and your app password as the password. In the resulting JSON body, we use pattern matching to get the did value and the JWT access token. The JWT access token will be required for all other calls that require authentication while the DID is the reference to the account you just logged in to.

%{"did" => did, "accessJwt" => accessJwt} = Req.post!(
  "https://bsky.social/xrpc/com.atproto.server.createSession",
  json: %{
    identifier: "yellowduck.be", # Your username with the @ sign
    password: "my-secret-password" # Your actual password
  }
).body

The result will look like this:

{
  "accessJwt": "string",
  "refreshJwt": "string",
  "handle": "string",
  "did": "string",
  "didDoc": {},
  "email": "string",
  "emailConfirmed": true,
  "emailAuthFactor": true,
  "active": true,
  "status": "takendown"
}

For the next steps, we just need the value in did and the token in accessJwt.

If something goes wrong, you'll get a HTTP 400 or 401 status code instead of a 200 status code with the error message.

Step 3: Create the post record

The next step is to create a record specifying what we want to post. The structure on how the post record should look like is described here.

# Create the current timestamp in ISO format with a trailing Z
created_at = DateTime.utc_now() |> DateTime.to_iso8601()

# Create the post record
record =
  %{
    text: "Hello from Elixir", # The text you want to post, max 300 graphemes
    createdAt: created_at, # The timestamp when this post was originally created.
    "$type": "app.bsky.feed.post", # Indicating this is a post on your feed
    langs: ["en-US"] # Indicating that the post is in English
  }

With this data structure defined, let's actually post it to your timeline.

Step 4: Post to Bluesky

The create a post on your timeline, we need to do a HTTP POST to the com.atproto.repo.createRecord endpoint. It takes again a JSON payload as the body and also requires you to use the JWT token as a Bearer authorization header.

%{"commit" => %{"rev" => rev}} =
  Req.post!(
    "https://bsky.social/xrpc/com.atproto.repo.createRecord",
    auth: {:bearer, token},
    json: %{
      repo: did, # The did value from step 1
      collection: "app.bsky.feed.post", # We want to post to our timeline
      record: record # The record we want to post to our timeline
    }
  ).body

Executing his will return the following data structure:

{
  "uri": "string",
  "cid": "string",
  "commit": {
    "cid": "string",
    "rev": "string"
  },
  "validationStatus": "valid"
}

As with the createSession endpoint, you'll get a HTTP 400 or 401 status code if something goes wrong instead of a 200 status code with the error message.

If all went fine, you should see the message show up in your feed now.

In the next part, we'll add tags to the mix.