RealWorld Conduit Implementation

repository·main·Indexed 42 days ago

https://github.com/team-alembic/realworld

A full-stack reference implementation of the RealWorld 'Conduit' specification built with the Ash Framework and Phoenix LiveView. It demonstrates advanced Elixir patterns including Ash domains, AshAuthentication, real-time updates via Ash.Notifier.PubSub, and a structured web layer using RealworldWeb macros.

Tokens
9.7K
Snippets
28
Records
46
Agent score
52%

What's inside RealWorld

  1. Understand the Domain Layer and Code Interfaces

    main

    The application logic is organized into three Ash domains. Each domain exposes a code interface consisting of plain functions. When calling these functions from the web layer, you must provide an actor: option to ensure authorization policies are enforced.

    DomainResourcesKey Concepts
    Realworld.AccountsUser, TokenAshAuthentication (registration, sign-in, token revocation), policy bypass vs. anonymous access, self-only updates
    Realworld.ArticlesArticle, Tag, ArticleTag, Comment, FavoriteDerived attributes (slug, sanitized markdown), manage_relationship for tags, paginated/filtered feeds, aggregates/calculations, PubSub notifiers, upserts
    Realworld.ProfilesFollowActor-scoped reads, idempotent upsert creates, policy-guarded destroys
  2. Key implementation details for Ash and Phoenix LiveView

    main

    When building on or extending this codebase, note these specific implementation patterns:

    • Article Body Handling: Articles store two bodies. Authors write markdown into body_raw. A RenderMarkdown change renders this into sanitized HTML in body (using MDEx). The frontend uses raw/1 on the body field. Note: No action accepts body or slug directly.
    • Default Authorization: All domains use authorize :by_default. Every resource declares its own policies. The Token resource is deny-by-default behind AshAuthentication's bypass check.
    • Real-time Updates: Real-time functionality is driven by the data layer. Comment and Favorite resources use Ash.Notifier.PubSub to broadcast on create/destroy. LiveViews subscribe to these updates per-article.
    • Asset Bundling: The Conduit theme is located at assets/vendor/main.css and is bundled by esbuild along with assets/css/app.css.
  3. Deploy the application

    main

    The application is deployed as a standard Phoenix release. In a production environment, the following environment variables must be configured:

    • SECRET_KEY_BASE: Used to sign cookies (generate with mix phx.gen.secret).
    • TOKEN_SIGNING_SECRET: Used to sign AshAuthentication tokens (generate with mix phx.gen.secret).
    • DATABASE_URL: The connection string for PostgreSQL.
    • PHX_HOST: The host for the Phoenix application.

    To build assets for deployment, run:

    mix assets.deploy
    mix phx.gen.secret
    mix assets.deploy
  4. Use tests as documentation for domain logic

    main

    The test suite is designed to serve as a worked example of the concepts demonstrated by each resource. To understand specific behaviors, refer to these files:

    • Article logic: test/realworld/articles/resources/article_test.exs (covers publishing, tag management, author-only policies, Ash.can?/2, and aggregates vs calculations).
    • Account logic: test/realworld/accounts/resources/user_test.exs (covers registration, sign-in, error handling, and self-only update policies).
    • Feed logic: test/realworld/articles/resources/list_articles_test.exs (covers filtering and pagination).
    • UI/LiveView logic: Located in test/realworld_web/live/ using PhoenixTest.
    • Data generation: test/support/data_case.ex demonstrates a pattern where actions are used as factories instead of using a separate fixture library.
  5. Install and set up the RealWorld project

    main

    To get the application running locally, ensure you have the correct Erlang and Elixir versions installed (refer to .tool-versions and use asdf install or mise install). You also need a recent version of PostgreSQL. Connection settings are located in config/dev.exs.

    Follow these steps to initialize the environment:

    1. Run mix setup to fetch dependencies, create/migrate the database, and seed demo data.
    2. Start the server with mix phx.server.
    3. Access the application at http://localhost:4000.

    Demo Credentials:

    • User: demo@example.com / Password: password123!
    • Other seeded users: alice, bob, and carol (all use password123!).

    To reset the database with the demo data at any time, run mix seed.

    mix setup
    mix phx.server
  6. Run tests and pre-commit checks

    main

    The project uses mix test to run the test suite, which automatically sets up a test database.

    Before pushing code, run mix precommit. This command executes a full gate including:

    • Compiling with warnings treated as errors
    • Unused dependency checks
    • Code formatting
    • The full test suite
    mix test
    mix precommit
  7. How RenderMarkdown changes work

    main

    The Realworld.Articles.Changes.RenderMarkdown module is an Ash.Resource.Change designed to transform user-supplied markdown into sanitized HTML.

    It acts as the security boundary for article content:

    1. It reads the :body_raw attribute from the changeset.
    2. It parses the markdown using MDEx with render: [unsafe: true] (allowing inline HTML through the parser).
    3. It immediately sanitizes the output using MDEx.Document.default_sanitize_options() to strip scripts and event handlers, preventing XSS.
    4. It writes the resulting HTML to the :body attribute using force_change_attribute/3.

    Note: The :body attribute is never accepted via user input; it is exclusively written by this change to ensure content integrity.

  8. Access HTML and LiveView helpers via RealworldWeb

    main

    When using use RealworldWeb, :html, :live_view, :live_component, or :component, the following helpers and imports are automatically made available to your module:

    • HTML & Forms: Phoenix.HTML, Phoenix.HTML.Form, and PhoenixHTMLHelpers (for tag builders like text_input/2, submit/2, etc.).
    • LiveView & Components: Phoenix.Component (for assigns, slots, <.form>, live_patch, etc.).
    • Project Specifics:
      • RealworldWeb.CoreComponents
      • RealworldWeb.Components
      • RealworldWeb.ErrorHelpers
      • RealworldWeb.LiveHelpers
    • Routing: Verified routes via the ~p sigil.
  9. Handle article favorite real-time updates

    main

    The PageLive.Index LiveView subscribes to real-time updates for article favorites. When a favorite is created or destroyed by another user, the LiveView catches the broadcast and updates the local favorites_count for the relevant article in the :articles list.

    Broadcast Topics

    • "favorite:created:<article_id>"
    • "favorite:destroyed:<article_id>"

    Note: The LiveView ignores broadcasts where the payload.from matches its own PID to prevent redundant local updates.

  10. Filter and sort article feeds using FilterSortFeed

    main

    The Realworld.Articles.Article.Preparations.FilterSortFeed module is an Ash.Resource.Preparation used to encapsulate the query logic for the :list_articles action. It handles optional filtering, personal feed restrictions, sorting, and data loading for article feed cards.

    Filtering Options

    When calling Articles.list_articles/2, you can pass a filter argument within the options map to filter by:

    • tag: Filters articles by a specific tag name.
    • author: Filters articles by a specific author ID.
    • favourited: Filters articles by a specific favorite ID.

    Personal Feed Logic

    If the private_feed? argument is set to true in the query, the results are restricted to articles from authors followed by the actor.

    Sorting and Loading

    • Sorting: Articles are automatically sorted by created_at: :desc (newest first).
    • Data Loading: The preparation loads :user, :tags, and :favorites_count. If an actor is provided, it also loads the is_favorited status specifically for that actor.
    # Example usage via the Articles resource
    # The web layer calls the action, and this preparation is applied automatically
    Articles.list_articles(%{filter: %{tag: "elixir"}}, actor: current_user)
  11. Real-time comment updates in ArticleLive.Index

    main

    The RealworldWeb.ArticleLive.Index LiveView subscribes to Phoenix PubSub topics to provide real-time updates to the article's comment list without requiring a page refresh.

    Subscribed Topics:

    • "comment:created:#{article_id}"
    • "comment:destroyed:#{article_id}"

    Handling Logic:

    • Creation: When a comment:created event with the create action is received, the new comment is appended to the :article.comments list in the socket assigns.
    • Destruction: When a comment:destroyed event with the destroy action is received, the comment is removed from the :article.comments list by filtering out the matching comment ID.
    # Internal handling of PubSub messages
    @impl true
    def handle_info(%{topic: "comment:created:" <> _, event: "create", payload: %{data: comment}}, socket) do
      socket = update(socket, :article, fn article ->
        Map.put(article, :comments, article.comments ++ [comment])
      end)
      {:noreply, socket}
    end
    
    @impl true
    def handle_info(%{topic: "comment:destroyed:" <> _, event: "destroy", payload: %{data: comment}}, socket) do
      socket = update(socket, :article, fn article ->
        comments = Enum.reject(article.comments, fn c -> c.id == comment.id end)
        Map.put(article, :comments, comments)
      end)
      {:noreply, socket}
    end
  12. Manage JavaScript dependencies in assets

    main

    There are two ways to include dependencies in the assets directory:

    1. Vendored approach: Place files in assets/vendor and import them using relative paths. import "../vendor/some-package.js"

    2. NPM approach: Run npm install some-package --prefix assets and import using the package name. import "some-package"