annot.at is a service for syncing static websites, such as blogs, to atproto and standard.site annot.at
elixir atproto standardsite
3

Configure Feed

Select the types of activity you want to include in your feed.

Add accounts models

Johanna Larsson (Jun 8, 2026, 9:41 PM +0100) c645fb18 9101cab0

+139 -14
+1
.gitignore
··· 35 35 npm-debug.log 36 36 /assets/node_modules/ 37 37 38 + .mix_tasks
+7 -14
README.md
··· 1 1 # AnnotAt 2 2 3 - To start your Phoenix server: 3 + A service for automatically publishing content to the Atmosphere, using the standard.site lexicon. Register your publication and a discovery mechanism, and your content is automatically published. 4 4 5 - * Run `mix setup` to install and setup dependencies 6 - * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 5 + ## TODO 7 6 8 - Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 - 10 - Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 - 12 - ## Learn more 13 - 14 - * Official website: https://www.phoenixframework.org/ 15 - * Guides: https://hexdocs.pm/phoenix/overview.html 16 - * Docs: https://hexdocs.pm/phoenix 17 - * Forum: https://elixirforum.com/c/phoenix-forum 18 - * Source: https://github.com/phoenixframework/phoenix 7 + - [ ] Sign up/sign in with Bluesky 8 + - [ ] Add site including verification 9 + - [ ] RSS poller 10 + - [ ] standard.site document reader 11 + - [ ] Post to Bluesky
+5
lib/annot_at/accounts.ex
··· 1 + defmodule AnnotAt.Accounts do 2 + @moduledoc """ 3 + Context module for accounts related database queries, such as looking up users. 4 + """ 5 + end
+46
lib/annot_at/accounts/atproto_session.ex
··· 1 + defmodule AnnotAt.Accounts.AtprotoSession do 2 + use Ecto.Schema 3 + 4 + import Ecto.Changeset 5 + 6 + schema "atproto_sessions" do 7 + # e.g. https://bsky.social 8 + field :auth_server_issuer, :string 9 + # Space-separated scopes from token response 10 + field :granted_scopes, :string 11 + field :access_token, :string 12 + field :refresh_token, :string 13 + # ES256 keypair for this session 14 + field :dpop_private_jwk, :string 15 + # Access token expiry 16 + field :expires_at, :utc_datetime 17 + 18 + belongs_to :user, AnnotAt.Accounts.User 19 + 20 + timestamps(type: :utc_datetime) 21 + end 22 + 23 + def changeset(session, attrs) do 24 + session 25 + |> cast(attrs, [ 26 + :user_id, 27 + :auth_server_issuer, 28 + :granted_scopes, 29 + :access_token, 30 + :refresh_token, 31 + :dpop_private_jwk, 32 + :expires_at 33 + ]) 34 + |> validate_required([ 35 + :user_id, 36 + :auth_server_issuer, 37 + :granted_scopes, 38 + :access_token, 39 + :refresh_token, 40 + :dpop_private_jwk, 41 + :expires_at 42 + ]) 43 + |> foreign_key_constraint(:user_id) 44 + |> unique_constraint(:user_id) 45 + end 46 + end
+43
lib/annot_at/accounts/user.ex
··· 1 + defmodule AnnotAt.Accounts.User do 2 + use Ecto.Schema 3 + 4 + import Ecto.Changeset 5 + 6 + schema "users" do 7 + # Stable atproto identity from OAuth sub 8 + field :did, :string 9 + # Cached display handle 10 + field :handle, :string 11 + # Cached from profile 12 + field :display_name, :string 13 + # Same 14 + field :avatar_url, :string 15 + # Cached PDS URL from DID resolution 16 + field :pds_host, :string 17 + # Last bidirectional handle↔DID check 18 + field :handle_verified_at, :utc_datetime 19 + 20 + has_one :atproto_session, AnnotAt.Accounts.AtprotoSession 21 + 22 + timestamps(type: :utc_datetime) 23 + end 24 + 25 + def changeset(user, attrs) do 26 + user 27 + |> cast(attrs, [ 28 + :did, 29 + :handle, 30 + :display_name, 31 + :avatar_url, 32 + :pds_host, 33 + :handle_verified_at 34 + ]) 35 + |> validate_required([:did]) 36 + |> validate_length(:did, max: 2048) 37 + |> validate_length(:handle, max: 255) 38 + |> validate_length(:display_name, max: 255) 39 + |> validate_length(:avatar_url, max: 2048) 40 + |> validate_length(:pds_host, max: 255) 41 + |> unique_constraint(:did) 42 + end 43 + end
+18
priv/repo/migrations/20260608073456_users.exs
··· 1 + defmodule AnnotAt.Repo.Migrations.Users do 2 + use Ecto.Migration 3 + 4 + def change do 5 + create table(:users) do 6 + add :did, :text, null: false 7 + add :handle, :text 8 + add :display_name, :text 9 + add :avatar_url, :text 10 + add :pds_host, :text 11 + add :handle_verified_at, :utc_datetime 12 + 13 + timestamps(type: :utc_datetime) 14 + end 15 + 16 + create unique_index(:users, [:did]) 17 + end 18 + end
+19
priv/repo/migrations/20260608073605_atproto_sessions.exs
··· 1 + defmodule AnnotAt.Repo.Migrations.AtprotoSessions do 2 + use Ecto.Migration 3 + 4 + def change do 5 + create table(:atproto_sessions) do 6 + add :user_id, references(:users, on_delete: :delete_all), null: false 7 + add :auth_server_issuer, :text, null: false 8 + add :granted_scopes, :text, null: false 9 + add :access_token, :text, null: false 10 + add :refresh_token, :text, null: false 11 + add :dpop_private_jwk, :text, null: false 12 + add :expires_at, :utc_datetime, null: false 13 + 14 + timestamps(type: :utc_datetime) 15 + end 16 + 17 + create unique_index(:atproto_sessions, [:user_id]) 18 + end 19 + end