Scrypath fits Phoenix best when your context is the application-facing boundary for search orchestration.

If you want shared controller and LiveView glue for browser-shaped params, keep that glue in Scrypath.Phoenix. It delegates to Scrypath.QueryParams and stops at plain data, URL params, and renderable edge errors. For the shared contract details, read Request-edge search.

What Belongs In The Context

Keep these responsibilities in the context:

What Stays Out Of Controllers And LiveView

Do not teach controllers or LiveView modules to compose raw Repo and Scrypath.* calls as the main pattern.

Helpers normalize params/forms/URLs only, contexts remain canonical, and Phoenix is optional.

That boundary matters because Phoenix web modules should stay focused on HTTP or UI concerns, while the context owns feature logic and operational decisions.

Example

defmodule MyApp.Content do
  alias MyApp.Blog.Post
  alias MyApp.Repo

  def search_posts(query, opts \\ []) do
    Scrypath.search(Post, query,
      Keyword.merge([backend: Scrypath.Meilisearch, repo: Repo], opts)
    )
  end

  def publish_post(post, attrs) do
    with {:ok, post} <- update_post(post, attrs),
         {:ok, _sync} <-
           Scrypath.sync_record(Post, post,
             backend: Scrypath.Meilisearch,
             sync_mode: :inline
           ) do
      {:ok, post}
    end
  end
end

MyApp.Content.search_posts/2 is the shared read path for controllers and LiveView. MyApp.Content.publish_post/2 is the write path that owns the sync decision.

Why This Boundary Holds Up

  • controllers stay thin
  • LiveView state stays UI-focused
  • sync failures and rebuild decisions stay close to the feature that owns them
  • docs and snippets can derive from one canonical example instead of teaching competing architectures