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: trueto enable it only in trusted operator contexts.
Installation
Add Lantern to your deps. From Hex:
def deps do
[{:lantern, "~> 0.3.0"}]
endOr straight from GitHub:
def deps do
[{:lantern, github: "go9/lantern"}]
endLantern 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"
/>
"""
endThen 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:
| Assign | Values | Purpose |
|---|---|---|
:class | string/list | Extra classes appended to the root .lantern element. |
:theme | :system, :light, :dark | Sets data-theme; :system follows prefers-color-scheme. |
:style | string | Inline style for quick CSS-variable overrides. |
Theme variables:
| Variable | Purpose |
|---|---|
--lt-bg, --lt-bg-subtle, --lt-bg-hover | Surfaces and hover states. |
--lt-fg, --lt-fg-muted | Text colors. |
--lt-border, --lt-ring | Borders and focus rings. |
--lt-accent, --lt-danger, --lt-success | Action/status colors. |
--lt-font, --lt-mono | UI and monospace fonts. |
--lt-text, --lt-text-sm | Base and small text sizes. |
--lt-radius, --lt-radius-sm | Corner radii. |
--lt-height | Fixed 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
:sourceyou control. Never build a:sourcefrom untrusted user input — that turns your server into an open SQL proxy. - Only enabling
allow_raw_filter: truein 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.