Unified preference management for Backpex.
Reads and writes UI state (theme, sidebar open/closed, sidebar section
expansion, per-resource column visibility, metric toggles, and user-defined
keys) through a configurable adapter. The adapter is selected per key by
a longest-prefix match against the configured routes (see
Backpex.Preferences.Router), so different prefixes can live in different
storage backends — e.g. global.* in the Phoenix session and resource.*
in a scoped database table.
Zero-config defaults
With no :backpex, Backpex.Preferences config set, every key routes to
Backpex.Preferences.Adapters.Session.
Configuring per-prefix routing
config :backpex, Backpex.Preferences,
adapters: [
{"global.*", Backpex.Preferences.Adapters.Session, []},
{"resource.*", Backpex.Preferences.Adapters.Ecto,
repo: MyApp.Repo, schema: MyApp.Preference, scope_fields: [:user_id, :tenant_id]},
{:default, Backpex.Preferences.Adapters.Session, []}
],
scope: {MyAppWeb.PreferencesScope, :resolve, []}Key format
global.*— application-wide preferences (theme, sidebar, ...).resource:<Module>:*— per-resource preferences (columns, metrics, ...). Uses:as a separator so module-name dots don't split into extra segments (Backpex.Preferences.Key).custom.*— user-defined preferences.
Public API
get/3— read a single preference, with a:defaultfallback.get_map/3— read every value under a prefix as a nested map.put/4— write from a LiveView socket or%Plug.Conn{}.put_batch/3— dispatch a list of writes (best-effort, first-error-wins; see the function docs for the partial-success semantics).
Summary
Functions
Applies a list of adapter side effects to a %Plug.Conn{}.
Reads a preference. Falls back to opts[:default] when the value is
missing or the adapter cannot resolve the current preference scope.
Reads every value under prefix as a nested map.
Persists a preference from within a LiveView socket or Plug controller.
Dispatches a batch of writes through their adapters and returns the collected side effects, or the first error encountered.
Returns the Phoenix session key used by the Session adapter.
Functions
Applies a list of adapter side effects to a %Plug.Conn{}.
Takes a list because a batch write collects one effect per entry (see
put_batch/3); a single put/4 yields at most one.
Exposed for the preferences controller; not intended for general callers.
Reads a preference. Falls back to opts[:default] when the value is
missing or the adapter cannot resolve the current preference scope.
Accepts a %Backpex.Preferences.Context{} or a bare Phoenix session map.
The session-map form is convenient for call sites that only have a
session on hand.
Options
:default— returned when nothing is stored forkey(default:nil).
Extra options are forwarded to the adapter.
Distinguishing "never set"
With no :default, a missing value reads as nil — which separates "the
user never set this" from "the user deliberately stored an empty value".
An explicitly cleared %{} or [] is a real preference and must not be
overwritten by an application default; this is how persist: [:filters]
decides whether to apply a resource's filter defaults.
The default Session adapter reserves nil for “not found”, so it cannot
distinguish a stored nil from a missing key. Store a tagged value such as
%{"value" => nil} when that distinction matters. A custom adapter that
returns {:ok, nil} may preserve nil; with such an adapter, a sentinel
default (for example default: :__unset__) distinguishes the missing case.
Examples
iex> session = %{"backpex_preferences" => %{"global" => %{"theme" => "dark"}}}
iex> Backpex.Preferences.get(session, "global.theme")
"dark"
iex> Backpex.Preferences.get(%{}, "global.theme", default: "light")
"light"
Reads every value under prefix as a nested map.
Keys in the returned map are relative to prefix (i.e. segments that
follow the prefix). The adapter is free to store values however it likes,
but the shape returned here matches what a single nested get/3 at that
prefix would have produced.
Returns %{} when nothing is stored, the adapter cannot resolve the scope,
or the adapter fails for any other reason.
Client-overlay descendants are reconstructed only for dot-form keys by
matching prefix <> ".". Adapter-backed colon-form subtrees remain readable,
but pending client values for keys such as resource:MyApp.PostLive:columns
are not merged into a get_map/3 result. Use get/3 for colon-form keys when
client-overlay precedence is required.
Examples
iex> session = %{
...> "backpex_preferences" => %{
...> "global" => %{"sidebar_section" => %{"blog" => true, "users" => false}}
...> }
...> }
iex> Backpex.Preferences.get_map(session, "global.sidebar_section")
%{"blog" => true, "users" => false}
iex> Backpex.Preferences.get_map(%{}, "global.sidebar_section")
%{}
Persists a preference from within a LiveView socket or Plug controller.
Resolves the adapter for key, asks it to persist the value, and applies
the side effect it returns (e.g. put_session) to the caller. An adapter
that persisted on its own returns {:ok, :persisted} and the caller is
handed back unchanged.
When the chosen adapter refuses a non-HTTP write with :requires_http
(default behavior of the Session adapter outside a controller), falls back
to push_event/3 so the browser can retry via the preferences controller
on its next paint.
A socket target supplies socket.assigns but no mount session to the
adapter/scope context (ctx.session is %{}). Scope resolution for
server-originated LiveView writes must therefore work from assigns. A conn
target supplies both conn.assigns and the current session.
Returns one of:
{:ok, socket_or_conn}— write accepted.{:error, reason}— the adapter refused the write for a non-transport reason. Callers typically ignore the failure (preferences are best effort) but can surface it if needed.
Options
:mirror—:sessionto have the browser mirror the value intosessionStorage. Only consulted on thepush_eventfallback: it is a property of that round-trip, not of the value, and an adapter that persists server-side is read fresh at every mount and needs no mirror. SeeBackpex.Preferences.LiveView.push_write/4for when a key needs it.
Every other option is forwarded to the adapter, on top of the adapter's configured options.
Examples
From a Plug controller (session is updated in-place):
Backpex.Preferences.put(conn, "global.theme", "dark")
#=> {:ok, %Plug.Conn{}}From a LiveView handle_event (session adapter returns :requires_http,
so the dispatcher falls back to a push_event for the browser to retry):
Backpex.Preferences.put(socket, "global.theme", "dark")
#=> {:ok, %Phoenix.LiveView.Socket{}}
Dispatches a batch of writes through their adapters and returns the collected side effects, or the first error encountered.
Used by Backpex.PreferencesController to dispatch cross-adapter batch
writes.
Each entry's adapter returns at most one side effect, so the returned list
holds one entry per write that needs the caller to do something — adapters
that persisted on their own ({:ok, :persisted}) contribute nothing.
Threads the accumulated session state through each adapter call so that
writes under the same session key compose correctly. The caller applies
the returned effects in order; for :put_session effects targeting the
same key, the last effect holds the fully-merged value.
Semantics
This is best-effort, first-error-wins. On the first adapter that
returns {:error, reason} the loop halts and returns
{:error, {key, reason}} — subsequent entries are not dispatched. Earlier
successful writes may already have been committed by their adapters (e.g.
a DB-backed adapter that writes eagerly). The adapter behaviour has no
rollback primitive, so callers should treat partial success as possible.
Examples
ctx = Backpex.Preferences.Context.from_conn(conn)
Backpex.Preferences.put_batch(ctx, [
{"global.theme", "dark"},
{"global.sidebar_open", false}
])
#=> {:ok, [{:put_session, "backpex_preferences", %{...}}]}
Returns the Phoenix session key used by the Session adapter.
Convenience passthrough to Backpex.Preferences.Adapters.Session.session_key/0.