···11# AnnotAt
2233-To start your Phoenix server:
33+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.
4455-* Run `mix setup` to install and setup dependencies
66-* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
55+## TODO
7688-Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
99-1010-Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
1111-1212-## Learn more
1313-1414-* Official website: https://www.phoenixframework.org/
1515-* Guides: https://hexdocs.pm/phoenix/overview.html
1616-* Docs: https://hexdocs.pm/phoenix
1717-* Forum: https://elixirforum.com/c/phoenix-forum
1818-* Source: https://github.com/phoenixframework/phoenix
77+- [ ] Sign up/sign in with Bluesky
88+- [ ] Add site including verification
99+- [ ] RSS poller
1010+- [ ] standard.site document reader
1111+- [ ] Post to Bluesky
+5
lib/annot_at/accounts.ex
···11+defmodule AnnotAt.Accounts do
22+ @moduledoc """
33+ Context module for accounts related database queries, such as looking up users.
44+ """
55+end
+46
lib/annot_at/accounts/atproto_session.ex
···11+defmodule AnnotAt.Accounts.AtprotoSession do
22+ use Ecto.Schema
33+44+ import Ecto.Changeset
55+66+ schema "atproto_sessions" do
77+ # e.g. https://bsky.social
88+ field :auth_server_issuer, :string
99+ # Space-separated scopes from token response
1010+ field :granted_scopes, :string
1111+ field :access_token, :string
1212+ field :refresh_token, :string
1313+ # ES256 keypair for this session
1414+ field :dpop_private_jwk, :string
1515+ # Access token expiry
1616+ field :expires_at, :utc_datetime
1717+1818+ belongs_to :user, AnnotAt.Accounts.User
1919+2020+ timestamps(type: :utc_datetime)
2121+ end
2222+2323+ def changeset(session, attrs) do
2424+ session
2525+ |> cast(attrs, [
2626+ :user_id,
2727+ :auth_server_issuer,
2828+ :granted_scopes,
2929+ :access_token,
3030+ :refresh_token,
3131+ :dpop_private_jwk,
3232+ :expires_at
3333+ ])
3434+ |> validate_required([
3535+ :user_id,
3636+ :auth_server_issuer,
3737+ :granted_scopes,
3838+ :access_token,
3939+ :refresh_token,
4040+ :dpop_private_jwk,
4141+ :expires_at
4242+ ])
4343+ |> foreign_key_constraint(:user_id)
4444+ |> unique_constraint(:user_id)
4545+ end
4646+end
+43
lib/annot_at/accounts/user.ex
···11+defmodule AnnotAt.Accounts.User do
22+ use Ecto.Schema
33+44+ import Ecto.Changeset
55+66+ schema "users" do
77+ # Stable atproto identity from OAuth sub
88+ field :did, :string
99+ # Cached display handle
1010+ field :handle, :string
1111+ # Cached from profile
1212+ field :display_name, :string
1313+ # Same
1414+ field :avatar_url, :string
1515+ # Cached PDS URL from DID resolution
1616+ field :pds_host, :string
1717+ # Last bidirectional handle↔DID check
1818+ field :handle_verified_at, :utc_datetime
1919+2020+ has_one :atproto_session, AnnotAt.Accounts.AtprotoSession
2121+2222+ timestamps(type: :utc_datetime)
2323+ end
2424+2525+ def changeset(user, attrs) do
2626+ user
2727+ |> cast(attrs, [
2828+ :did,
2929+ :handle,
3030+ :display_name,
3131+ :avatar_url,
3232+ :pds_host,
3333+ :handle_verified_at
3434+ ])
3535+ |> validate_required([:did])
3636+ |> validate_length(:did, max: 2048)
3737+ |> validate_length(:handle, max: 255)
3838+ |> validate_length(:display_name, max: 255)
3939+ |> validate_length(:avatar_url, max: 2048)
4040+ |> validate_length(:pds_host, max: 255)
4141+ |> unique_constraint(:did)
4242+ end
4343+end
+18
priv/repo/migrations/20260608073456_users.exs
···11+defmodule AnnotAt.Repo.Migrations.Users do
22+ use Ecto.Migration
33+44+ def change do
55+ create table(:users) do
66+ add :did, :text, null: false
77+ add :handle, :text
88+ add :display_name, :text
99+ add :avatar_url, :text
1010+ add :pds_host, :text
1111+ add :handle_verified_at, :utc_datetime
1212+1313+ timestamps(type: :utc_datetime)
1414+ end
1515+1616+ create unique_index(:users, [:did])
1717+ end
1818+end