Installation

Add PhoenixLens to mix.exs:

def deps do
  [
    {:phoenix_lens, "~> 0.1.4"}
  ]
end

Configure a database and the fields that must never appear in outputs:

config :phoenix_lens,
  repo: MyApp.Repo,
  masked_fields: [:email, :first_name, :last_name, :phone]

Ecto schema fields with redact: true are protected automatically in addition to this list.

Mount the dashboard in router.ex. Always put it behind authentication in production.

import PhoenixLensWeb.Router

scope "/" do
  pipe_through [:browser, :require_admin]
  lens "/lens"
end

Create the tables:

defmodule MyApp.Repo.Migrations.AddPhoenixLens do
  use Ecto.Migration

  def up, do: PhoenixLens.Migrations.up()
  def down, do: PhoenixLens.Migrations.down()
end

Then open /lens. Screenshots and query-engine details: Guide.md. MCP server: MCP.md.

The SQL editor autocompletes tables and columns from Ecto schemas (and information_schema when a repo is configured). Type after FROM / JOIN for tables, users. for that table’s columns, or a prefix in SELECT / WHERE. Tab or Enter inserts. Protected columns are labelled redacted.

Parent scopes are included in generated URLs.

The host :browser pipeline should include :fetch_session, :fetch_live_flash, and :protect_from_forgery. The host endpoint must already have a LiveView socket at /live.

Authentication

PhoenixLens does not authenticate by itself when you mount it in a Phoenix pipeline. Put it behind your admin plug.

Optional HTTP basic auth:

config :phoenix_lens,
  username: System.get_env("PHOENIX_LENS_USERNAME"),
  password: System.get_env("PHOENIX_LENS_PASSWORD")

Do not expose this dashboard on the public internet without auth.

Settings → Security can add an extra UI lock: TOTP from an authenticator app, and/or passkeys. When either is enabled, /lens redirects to /lens/unlock until the session is unlocked. MCP requests are not gated by this lock. It is not a replacement for the host pipeline.

If you lock yourself out:

iex> PhoenixLens.Auth.reset!()

Passkeys use WebAuthn against the page origin (http://localhost:4000 in the dummy app). HTTPS is required everywhere except localhost.

Multiple databases

config :phoenix_lens,
  repo: MyApp.Repo,
  databases: [
    primary: [repo: MyApp.Repo],
    analytics: [url: System.get_env("ANALYTICS_DATABASE_URL"), name: "Analytics"]
  ]

Questions, dashboards, settings, and the audit log always persist on repo:. Point the query target at a read replica when you can. Audit is paginated; Settings sets how long rows are kept (7–365 days, or forever). Default is 90 days.

Column protection (/lens/settings/protection) adds runtime masks on top of masked_fields and Ecto redact: true: globally, per database/source, or per table.

MCP (/lens/settings/mcp) issues project tokens for the agent endpoint. See below.

MCP

After Plug.Parsers in the host endpoint, mount the MCP server so agents can use the same notebook behind a project token:

plug PhoenixLensWeb.Plugs.MCP, path: "/lens/mcp"

Issue tokens in Settings → MCP. Clients POST JSON-RPC to {mount}/mcp with Authorization: Bearer <secret>. Field policy still applies. Full tool list: MCP.md. Product tour: Guide.md.

Alerts

Settings → Integrations stores SMTP and named webhook URLs. On a saved question, Alert checks the question on a schedule (1m–daily) and emails or POSTs JSON when:

  • it returns any rows (or no rows)
  • a numeric value goes above or below a goal

Optional {:gen_smtp, "~> 1.2"} for outbound mail. Webhooks use :httpc. See Alerts.md.

DuckDB engine

/lens/settings can switch the query engine from PostgreSQL to DuckDB. DuckDB attaches the host Repo read-only as repo and any extra sources you add (Postgres, MySQL, SQLite, DuckDB files, Parquet, CSV, JSON). Join them in one question (repo.users, billing.orders, a CSV view). The host app must depend on {:duckdbex, "~> 0.4"}. User SQL is still SELECT-only; ATTACH is not allowed in the editor.

Embed a question

<.live_component
  module={PhoenixLens.Components.QuestionCard}
  id={"q-#{id}"}
  question_id={id}
  actor={@current_user}
/>

The same field policy applies.