An embeddable Postgres table viewer and editor for Phoenix LiveView. Hand it a database connection and drop the component into any LiveView — you get a schema selector, sidebar of tables, a sortable/filterable grid, inline editing, row insertion, bulk delete, type-aware inputs, foreign-key lookups, and fullscreen mode.

  • Drop-in — one live_component, a stylesheet, and a JS hook. No Fluxon, no icon library, no design-system assumptions.
  • Connection-agnostic — point it at any Postgres via a postgres:// URL, a keyword list, a map, or a struct. It opens short-lived connections; there's no pool for you to supervise.
  • Safe writes — every value is sent as a cast text parameter ($1::text::int4), never interpolated into SQL. Edits and deletes are scoped to the row's primary key; a table without one is insert-only (you can add rows, but not edit or delete existing ones).
  • Schema editing — create and drop tables, add/rename/drop columns, and rename tables from the UI or the data API. Identifiers are always quoted and column types pass an allowlist, so DDL can't be used for injection.
  • Themeable — all styling is semantic lt-* classes in a low-priority cascade layer, driven by --lt-* CSS variables. Override a handful of variables, or any class, with zero specificity fights.

⚠️ Lantern exposes whatever database you connect it to, including every column. It is meant for operators/admins. Always put it behind your own authentication and authorization — see Security. The raw SQL filter input is disabled by default; pass allow_raw_filter: true to enable it only in trusted operator contexts.

Installation

Add Lantern to your deps. From Hex:

def deps do
  [{:lantern, "~> 0.3.0"}]
end

Or straight from GitHub:

def deps do
  [{:lantern, github: "go9/lantern"}]
end

Lantern needs phoenix_live_view ~> 1.1, postgrex, and jason (all pulled in transitively). LiveView 1.1 is required because dialogs render through Phoenix.Component.portal/1.

Quick start

Render the component in any LiveView, passing a :source:

def render(assigns) do
  ~H"""
  <.live_component
    module={Lantern.Explorer}
    id="db"
    source={"postgres://user:pass@localhost:5432/my_db"}
    title="My database"
  />
  """
end

Then wire up the CSS and the JS hook. That's it.

Connection sources

:source is anything Lantern.Source.from/1 accepts:

# URL string
"postgres://user:pass@host:5432/my_db?sslmode=require"

# keyword list / map
[hostname: "localhost", port: 5432, username: "postgres",
 password: "postgres", database: "my_db"]

# a struct/record exposing host(name)/port/username/password/database,
# e.g. one you already use to describe a tenant or branch database
%MyApp.Database{host: "...", port: 5432, username: "...", password: "...", database: "..."}

Styling

Import the bundled stylesheet so the explorer looks good out of the box. With esbuild/Tailwind, import it from the dep in your app.css:

@import "../../deps/lantern/priv/static/lantern/lantern.css";

It's self-contained and lives in a low-priority @layer lantern, so any rule you write outside that layer overrides it. Re-theme by setting --lt-* variables on .lantern (or a parent):

.lantern {
  --lt-accent: #e0552d;
  --lt-radius: 0.75rem;
  --lt-height: 720px;     /* fixed shell height */
  --lt-font: "Inter", sans-serif;
  --lt-mono: "JetBrains Mono", monospace;
}

You can also set styling hooks directly on the component:

<.live_component
  module={Lantern.Explorer}
  id="db"
  source={...}
  class="my-admin-db"
  theme={:dark}
  style="--lt-accent: #e0552d; --lt-height: 720px;"
/>

Styling assigns:

AssignValuesPurpose
:classstring/listExtra classes appended to the root .lantern element.
:theme:system, :light, :darkSets data-theme; :system follows prefers-color-scheme.
:stylestringInline style for quick CSS-variable overrides.

Theme variables:

VariablePurpose
--lt-bg, --lt-bg-subtle, --lt-bg-hoverSurfaces and hover states.
--lt-fg, --lt-fg-mutedText colors.
--lt-border, --lt-ringBorders and focus rings.
--lt-accent, --lt-danger, --lt-successAction/status colors.
--lt-font, --lt-monoUI and monospace fonts.
--lt-text, --lt-text-smBase and small text sizes.
--lt-radius, --lt-radius-smCorner radii.
--lt-heightFixed explorer shell height.

Force dark mode with theme={:dark} or data-theme="dark"; force light mode with theme={:light} or data-theme="light". Omit it to follow the OS setting.

Using Tailwind? Optionally import the preset to map Lantern's variables onto your Tailwind color scale:

@import "../../deps/lantern/priv/static/lantern/lantern.css";
@import "../../deps/lantern/priv/static/lantern/lantern.tailwind.css";

JavaScript hook

Column resizing, the "set NULL" buttons, and live JSON validation use a LiveView hook. Register it on your LiveSocket. With esbuild, point at the dep:

import { LanternGrid } from "../deps/lantern/priv/static/lantern/hooks"

const liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: { LanternGrid },
})

(Browsing and editing still work without the hook — you just lose resizing, one-click NULL, and the JSON syntax highlight.)

Headless data API

The data layer is usable on its own, without the component:

{:ok, schemas} = Lantern.list_schemas(source)
{:ok, tables}  = Lantern.list_tables(source)                # defaults to schema: "public"
{:ok, tables}  = Lantern.list_tables(source, schema: "ops")
{:ok, stats}   = Lantern.table_stats(source, schema: "ops") # total/table/index sizes
{:ok, columns} = Lantern.columns(source, "users")          # name, type, nullable, enum, fk
{:ok, page}    = Lantern.query(source, "users",
                   schema: "public",
                   filters: [%{column: "email", op: "contains", value: "@example.com"}],
                   sort_by: "inserted_at", sort_dir: :desc,
                   count: false,
                   limit: 50, offset: 0)

{:ok, row}     = Lantern.insert(source, "users", %{"email" => "a@b.co"})
{:ok, updated} = Lantern.update(source, "users", %{"name" => "Ada"}, %{"id" => "1"})
{:ok, count}   = Lantern.delete(source, "users", [%{"id" => "1"}, %{"id" => "2"}])

Write values are passed as strings (or nil for SQL NULL) and cast to each column's type by Postgres.

Pass schema: "..." to reads, writes, and DDL calls to target non-public Postgres schemas. Identifiers are schema-qualified and quoted safely.

Schema changes (DDL) are available too. Identifiers are quoted and types are checked against an allowlist before anything touches the database:

:ok = Lantern.create_table(source, "widgets", [
        %{name: "id", type: "bigserial", nullable: false, primary_key: true},
        %{name: "label", type: "text"}
      ])
:ok = Lantern.add_column(source, "widgets", %{name: "qty", type: "integer"})
:ok = Lantern.rename_column(source, "widgets", "label", "name")
:ok = Lantern.drop_column(source, "widgets", "qty")
:ok = Lantern.rename_table(source, "widgets", "gadgets")
:ok = Lantern.drop_table(source, "gadgets")

Security

Lantern runs arbitrary reads and writes against the database you give it. By default, the raw SQL filter input is hidden (allow_raw_filter: false), because a literal SQL fragment appended after WHERE can execute data- modifying CTEs, sub-selects, and other arbitrary SQL under the connection role's privileges — that's an open SQL proxy in disguise.

For operator-facing pages where you trust the user, opt in:

<.live_component module={Lantern.Explorer} id="db" source={...} allow_raw_filter={true} />

You are responsible for:

  • Gating the page behind authentication/authorization (e.g. an admin pipeline).
  • Only handing Lantern a :source you control. Never build a :source from untrusted user input — that turns your server into an open SQL proxy.
  • Only enabling allow_raw_filter: true in trusted operator contexts.

Because the connection is operator-supplied, Lantern itself adds no extra sandboxing; the trust boundary is your auth layer.

Read-only deployments

For viewers you don't fully trust (a public dashboard, a support read-out), pass read_only: true:

<.live_component module={Lantern.Explorer} id="db" source={...} read_only={true} />

It hides every write affordance (inline edit, row insert, bulk delete, and all DDL) and refuses the matching events server-side, and restricts the SQL workspace to SELECT/EXPLAIN. Browsing, filtering, sorting, foreign-key navigation, charts, and exports stay available.

Local demo

Lantern ships with a local-only Phoenix demo app backed by a disposable Postgres fixture database:

cd examples/demo
docker compose up -d
mix setup
mix phx.server

Open http://localhost:4001. See examples/demo/README.md for details.

Development

mix deps.get
mix test                      # unit tests (no database needed)

Integration tests run real Postgres round-trips and are excluded by default. Point them at a database you can create/drop tables in:

LANTERN_TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/lantern_test \
  mix test --include integration

License

MIT © Giovanni Orlando. See LICENSE.