LibreDB Studio Field Guide
v0.16.1 MIT licensed 16 engines

Install, then a tour of every screen

One browser tab for every database you already run

Una pestaña del navegador para todas las bases de datos que ya tienes

すでに動かしているすべてのデータベースを、ブラウザーのタブ一つで

Одна вкладка браузера для всех баз данных, которые у вас уже работают

آپ کے پاس پہلے سے چل رہے تمام ڈیٹابیس، براؤزر کے ایک ہی ٹیب میں

一个浏览器标签页,管住你手上所有数据库

一個瀏覽器分頁,管住你手上所有資料庫

Jedna karta przeglądarki do każdej bazy danych, którą i tak już utrzymujesz

Un onglet de navigateur pour toutes les bases de données que vous exploitez déjà

Una scheda del browser per ogni database che già fai girare

Ein Browser-Tab für jede Datenbank, die du ohnehin schon betreibst

Satu tab browser untuk semua database yang sudah Anda jalankan

Uma aba do navegador para cada banco de dados que você já roda

LibreDB Studio is a self-hosted SQL IDE. You point it at databases you already have, and it gives you a schema explorer, a real SQL editor, a data grid you can edit in place, charts, ER diagrams, a query audit trail and a read-only database agent. All of it under MIT, with nothing held back for a paid tier.

Version covered
0.16.1Everything in this guide was checked against a running 0.16.1 container.
Runs as
One containerPort 3000 inside. No sidecar to run, though on first boot it does seed two sample databases into its data directory.
Engines with a driver
16Reaching 42 named engines in all, once the engines that speak the same wire protocol are counted.
Footprint
~384 MB heapThe image sets a Node heap cap of 384 MB, so it fits on a small NAS or a 1 GB droplet.

Install

Pick where you are running it

Choose one. The rest stay out of your way.

Docker Compose Railway Fly.io Render Koyeb DigitalOcean AWS Azure Google Cloud Helm Kubero Sealos OperatorHub Rancher Unraid TrueNAS SCALE CasaOS CapRover Dokploy Cosmos Proxmox Your own machine

Docker Compose

Any Linux, macOS or Windows host with Docker. Five minutes.

Docker Compose is the path I used to capture this guide, so it is the one described in most detail.

What you need

Docker with the Compose plugin, and a free port. That is the whole list. You do not need to clone the repository, and nothing is built from source.

The shortest possible start

If you only want to look at it, one command is enough:

docker run -p 3000:3000 ghcr.io/libredb/libredb-studio:0.16.1

Open http://localhost:3000. On a first run with no credentials set, the app generates an administrator password and prints it to the container log. Read it with docker logs.

The version you actually want to keep

For anything that outlives the afternoon, give it a compose file so the settings and the stored connections survive a restart. This is close to the stack I ran while writing the guide:

# compose.yaml
services:
  studio:
    image: ghcr.io/libredb/libredb-studio:0.16.1
    container_name: libredb-studio
    ports: ["3000:3000"]
    environment:
      ADMIN_EMAIL: admin@libredb.org
      ADMIN_PASSWORD: choose-something-long
      JWT_SECRET: "CHANGE_ME"   # openssl rand -base64 32
    volumes:
      - studio-data:/app/data
    restart: unless-stopped

volumes:
  studio-data:

The JWT_SECRET above is deliberately too short to work. Paste the real output of openssl rand -base64 32 in its place. Leave the placeholder and the app refuses to start and tells you why: JWT_SECRET is too short. That refusal is the behaviour you want, because a secret printed in a guide is a secret everybody has.

docker compose up -d
docker compose logs -f studio

Which image to pin

The image line above names the default tag, and two siblings are published beside it. All three are the same application. ghcr.io/libredb/libredb-studio:0.16.1 is built on Debian and carries every driver, including Oracle's Thick mode, which needs Oracle Instant Client and has no musl build. :0.16.1-alpine is the same product on musl and gives up only that: Oracle Thin still connects. :0.16.1-alpine-slim is the smallest and the only one that trades a feature for size. It ships without the DuckDB driver, whose shared library is the largest removable thing in the image, and opening a DuckDB connection there does not fail as though the engine were unreachable — it answers DuckDB is not available in this deployment and then names the remedy: the libredb-studio -alpine-slim image leaves it out to stay small; the default and -alpine tags ship it. Pulled on the machine that wrote this page, Docker reports the three at 903 MB, 530 MB and 259 MB, so the smallest is under a third of the default; your own figures will differ with the architecture you pull for. latest, latest-alpine and latest-alpine-slim move with each release, and the immutable sha- tags carry the same suffixes. The Helm chart's default stays the Debian tag; set image.tag to pin a variant.

What you should see

The container picks its own bind address before Next.js prints anything, so the first line of the log is about the socket rather than the app. It prefers ::, which serves IPv4 and IPv6 through one listener, and prints http://[::1]:3000 for the local address rather than the word localhost. Set HOSTNAME to pin it. The log settles within a few seconds and the health endpoint answers:

libredb-studio: bind address :: (dual-stack verified)
▲ Next.js 16.3.4
- Local:         http://[::1]:3000
- Network:       http://[::]:3000
✓ Ready in 0ms

$ curl -s localhost:3000/api/db/health
{"status":"healthy","timestamp":"...","service":"libredb-studio"}

Then open http://localhost:3000, sign in with the email and password you set, and skip ahead to the tour.

Trap 1 of 2

Two lines are worth adding once you know you need them. Set AUTH_COOKIE_SECURE to false if you will reach the app over plain HTTP on anything that is not localhost, and set it back to true the day HTTPS sits in front. Set STORAGE_PROVIDER to sqlite if more than one person will use it, so saved connections live on the volume instead of in one browser.

Trap 2 of 2

Three paths answer a liveness probe in the same shape, differing only in the timestamp they carry: /health, /api/health and /api/db/health. Point your orchestrator at any of them. Most of /api/ is not a substitute: some paths redirect to the login screen and answer 200 from there, which tells you nothing about the app, while others answer 401 or serve unrelated data. A GET at the health path answers the liveness question, not the readiness one; it reports that the service is up without opening a database connection. A POST to /api/db/health, with a connection in the body, is the one that actually reaches a database; the other two answer 405 to a POST.

Two more things worth setting

JWT_SECRET has to be at least 32 characters. A shorter value is fatal on purpose: the server refuses to start rather than serve a deployment where every login fails. On the server-side stores, sqlite and postgres, it also derives the key that encrypts saved database passwords, so rotating it leaves those connections in place with the password field dropped. Set STORAGE_ENCRYPTION_KEY if you want to rotate one without disturbing the other.

STORAGE_PROVIDER decides where saved connections live. The default, local, keeps them in the browser, which is fine for one person and useless for a team. sqlite puts them on the volume. postgres puts them in a database you name with STORAGE_POSTGRES_URL. Worth knowing before you decide: credential encryption is installed on the server-side stores only, so under the default local the saved passwords sit in the browser in plain text.

Worth knowing before you need it: a local account can require a six-digit code as well as its password. Set ADMIN_TOTP_SECRET to a base32 secret and the password alone stops being enough, which the login answers as mfaRequired until the code arrives. USER_TOTP_SECRET does the same for the second account. Both are optional and nothing changes if you leave them out.

Two groups of variables the other panels never need are worth naming here, because every deployment on this page ends in a login page on an address somebody else can reach. The first is the rate limiter, five buckets, each a count and a window in seconds: failed logins per client address (RATE_LIMIT_LOGIN_MAX, RATE_LIMIT_LOGIN_WINDOW_SEC) default five per three hundred seconds; failed logins per submitted account, twenty per three hundred; AI and agent requests per signed-in user, twenty per sixty; database-reaching requests per user, a hundred and twenty per sixty; and audit lines for refusals, five per three hundred. Setting a count to zero disables that bucket. The second group decides what the limiter counts against: TRUST_PROXY_HEADERS defaults to true and reads the client address from X-Forwarded-For, and TRUSTED_PROXY_HOPS defaults to zero, which takes the leftmost entry — the one the caller writes. Behind a proxy, set it to the number of proxies in front, or a caller picks their own bucket and the audit log's address field is fiction.

DigitalOcean

1-Click Droplet from the Marketplace. Ubuntu 24.04 LTS. The listing ships 0.16.0, one release behind the 0.16.1 these pages describe, so a detail here and there will not match. The first-boot script sets AUTH_COOKIE_SECURE=false for you, which is what a droplet answering on plain HTTP needs: without it the session cookie is marked Secure, the browser withholds it, and the login screen returns you to itself.

The clicks

  1. Open the LibreDB Studio listing in the DigitalOcean Marketplace.
  2. Press Create LibreDB Studio Droplet.
  3. Pick a region and a size. The snapshot is built on a 25 GB disk, so the droplet has to have at least that much: the 10 GB shared-CPU size cannot take it.
  4. Add your SSH key and create.

What you should see

Give first boot a minute. It generates credentials unique to that droplet and starts the container under a systemd unit. Then open http://your-droplet-ip:3000.

To get the credentials, SSH in as root; the message of the day says where they are. The droplet generates two accounts, admin@libredb.org and user@libredb.org, each with its own password, and both sit in /etc/libredb-studio.env. How that file is written is worth knowing if anyone else has a login on the box. The listing ships 0.16.0, and the script that release runs creates the file with a plain redirect and narrows the mode afterwards, and cloud-init runs that script under a umask of 0022 — so for the moment between the two the file is world-readable with live passwords in it. 0.16.1 writes it under umask 077 into a temporary name and then moves it into place, so the finished name is the only state anyone else can see. Rotate both passwords after first boot.

Change it, then lock the port down

There is no in-app password change: rotation means editing /etc/libredb-studio.env on the droplet and restarting the service. Put TLS in front before you use it for anything real. A reverse proxy or a DigitalOcean load balancer in front is what lets the browser keep the session cookie, and it is also how the traffic stops being cleartext. One thing to know while you do that: ufw will not filter port 3000, because Docker publishes it through its own iptables rules. Use a cloud firewall on the droplet instead.

The image is close to the AWS and Azure ones in shape: Ubuntu 24.04, the published container, run by systemd, credentials generated on your own instance at first boot. One difference is worth knowing: this droplet pins the container by tag, while the AWS image and the Azure template both pin it by digest. The one place where what you can boot today falls short of the AWS image is the write above. Nothing phones home and the publisher has no access to your droplet.

AWS

Free AMI product on AWS Marketplace, and the most current of the cloud images. More than one version stays selectable in the launch form, so pick the newest deliberately rather than taking whatever the page offers first.

What it costs

Nothing for the software: it is a free AMI-based product, with no metering, no contract and no software charge. AWS still bills you for what it runs on, which is the instance hours, the EBS volume and data transfer out, and anything else you attach to it.

The clicks

  1. Open the Marketplace listing and subscribe.
  2. Launch the AMI with the recommended security group: TCP 3000 and TCP 22, both scoped to your own CIDR.
  3. No user data, no IAM role, no AWS credentials. The instance needs none of them.

Finding the generated password. The image boots in strict mode — its first-boot script writes AUTH_BOOTSTRAP=off — so the password is generated once and never regenerated. Clearing the environment file does not produce a new one the way it does on a plain container; it produces a refusal.

First boot takes about a minute and generates a password unique to that instance. There are two ways to read it:

  • Without an SSH key: EC2 console, Actions, Monitor and troubleshoot, Get system log. The first-boot banner is printed there.
  • With SSH: log in as ubuntu and run sudo cat /etc/libredb-studio.info.

Then open http://your-instance-ip:3000 and sign in as admin@libredb.org.

Who else can read that password

The system log is the one place the generated password is published, so anyone holding ec2:GetConsoleOutput on the account can read it. Change it at once. Edit /etc/libredb-studio.env with an editor, not with sed, because a password containing / or & will corrupt the line, then run sudo systemctl restart libredb-studio.

Where your data lives

What Where
Saved connections and query history. The AMI runs on sqlite storage, so seven fields on every saved connection are sealed with AES-256-GCM before they are written: the database password, a connection string that can embed one, the agent-profile password, the TLS client private key, and the SSH password, private key and passphrase. Host, port, user, database and the TLS certificates stay readable on purpose, so that whoever holds a dump can still tell which databases are in it. /opt/libredb/data/libredb-storage.db
Administrator password and JWT_SECRET /etc/libredb-studio.env, mode 0600. docker inspect libredb-studio prints the same values back in cleartext to anyone who can reach the Docker socket, so treat shell access to the instance as access to the credentials.
First-boot banner /etc/libredb-studio.info (mode 0600)

Do not rotate JWT_SECRET on a working instance. The key that seals your saved connection secrets is derived from it, so changing it makes every one of them unreadable.

Azure

Solution template on Microsoft Marketplace. HTTPS is a switch in the wizard, on by default.

The clicks

  1. Open the Marketplace listing and press Get It Now.
  2. Work through the Azure portal wizard. It asks for a resource group, a region, a VM name and size, an OS disk size, your administrator details, a DNS name label, and whether to enable HTTPS with a free certificate. Two of its fields decide who can reach the machine at all, and they are easy to skim past: the allowed source for the web interface, and the allowed source for SSH.
  3. Deploy and wait for the template to finish.

What you should see

The template deploys one Ubuntu 24.04 LTS VM running the container behind a Caddy reverse proxy with automatic HTTPS. That is the one difference worth knowing about compared with the other cloud images: because Caddy terminates TLS for you, you reach the app at https:// and the cookie trap does not apply. Leave AUTH_COOKIE_SECURE alone here.

The deployment outputs in the portal give you the address to open. Sign in with the application administrator email and password you typed in the wizard, which are a separate pair from the VM's own administrator login you use for SSH.

The listing charges nothing for the software itself. Azure still bills the VM, the managed disk and outbound bandwidth, plus the public IP and anything else the template creates in the resource group, which is why the listing page shows a price that varies rather than a flat zero. One thing to check before you rely on it: the template pins the container by digest rather than by tag, and the digest in the package that is live today resolves to 0.16.0, the same release DigitalOcean serves and one behind the 0.16.1 these pages describe. A digest cannot drift, so what deploys next month is what deploys now; the version panel on the running instance is where you confirm it.

Google Cloud

Kubernetes app in the Marketplace catalog.

What you need

This channel deploys into Kubernetes rather than onto a VM, so you need an existing GKE cluster and the Kubernetes Engine Admin role on the target project. It is not the only Kubernetes route: the public Helm chart and the Rancher partner catalogue reach a cluster too, and the Helm block below is the same application by a shorter path.

The clicks

  1. Open the Marketplace catalog inside the Google Cloud console and search for LibreDB Studio.
  2. Press Configure, choose the target cluster and namespace.
  3. Deploy, then follow the post-deploy instructions Google shows to reach the service.
The version number is not the app version

Google requires the chart and the image tags of a Terraform Kubernetes app to share a major and minor version, and the repository's chart version and the application version do not match. The published copy is therefore packaged with a number of its own. The Marketplace listing currently reads 0.10, which is not comparable with a release tag and should not be read as one. It is not an old build.

If you would rather use Helm directly

The public chart is not the Marketplace one, but it installs the same application: chart 0.1.66 carries app version 0.16.1. Three of its values are worth knowing, all defaulting to empty, so a values file that sets none of them changes nothing. secrets.adminTotpSecret and secrets.userTotpSecret turn on the second factor for an account. config.basePath prefixes the health probes. Read that last one carefully before planning around it, because the subpath itself is fixed when the image is built and the published image is built at the root. The chart exposes no ingress by default, so the last line is how you reach it while you look around:

helm repo add libredb https://libredb.org/libredb-studio/
helm repo update
helm search repo libredb/libredb-studio   # chart and app version
helm install libredb libredb/libredb-studio

# the generated admin credentials
kubectl logs deployment/libredb-libredb-studio | grep -A 4 "generated admin credentials"

# the chart exposes no ingress by default, so reach it with a port-forward
kubectl port-forward deployment/libredb-libredb-studio 3000:3000

The chart exposes the cookie switch as config.authCookieSecure and leaves it unset, so the application decides: Secure in production. A chart install usually sits behind an ingress terminating TLS, which is why unset is the right default. Set it to false only if you are reaching the service over plain HTTP.

Unraid

Community Applications. No terminal. The template installs 0.16.0, behind the 0.16.1 these pages describe.

What you need

An Unraid server with the Community Applications plugin installed. Nothing else.

The clicks

  1. Open the Apps tab.
  2. Search for LibreDB Studio and press Install.
  3. Fill in the three required fields in the Add Container form: ADMIN_EMAIL, ADMIN_PASSWORD and JWT_SECRET. Only the last has a length the server checks, and it is 32 characters.
  4. Press Apply and wait for the pull.

What you should see

The WebUI button on the Docker tab opens http://your-unraid-host:3006. Host port 3006 is the template default, mapped to container port 3000. The template deliberately avoids 3000 on the host because it collides with other Unraid apps so often. Any free port works if you want a different one.

Sign in with the email and password you typed into the form.

Where the password is, and is not

On Unraid there is no generated password in the log: generation short-circuits once ADMIN_PASSWORD and JWT_SECRET are both set, so no auth-bootstrap.json is written. Keep what you typed. If you lose it, docker inspect libredb-studio on the box prints the environment back, and Unraid keeps the template on the flash drive.

What to back up

App data is /mnt/user/appdata/libredb-studio, mapped to /app/data. That directory holds the SQLite store with your saved connections and query history, and it is what survives a container update or recreate. Back up that directory and keep your credentials somewhere separate.

The template ships AUTH_COOKIE_SECURE=false already, because a LAN install is served over plain HTTP. If you later put the app behind a reverse proxy with HTTPS, flip it to true. Leaving it false on a host that faces the internet sends your session cookie in cleartext.

TrueNAS SCALE

Community train. Catalog version 1.0.11 ships app version 0.16.0, behind the 0.16.1 these pages describe.

What you need

TrueNAS SCALE with its apps catalog, and a dataset you are happy to hand to the app.

The clicks

  1. Apps, then Discover Apps, then search for LibreDB Studio.
  2. Press Install.
  3. Under the LibreDB section, set Admin Email, Admin Password (8 characters minimum) and JWT Secret (32 characters minimum).
  4. Under Storage, choose an ix-volume or point it at a host path for the data directory.
  5. Leave the rest alone and install.

What you should see

The WebUI port defaults to 30469, not 3000, which is normal for the TrueNAS apps catalog. Open the portal link from the app card, or go to http://your-nas:30469 directly.

The app runs as uid and gid 568, which is the standard apps user, so the dataset you pick needs to be readable and writable by it. TrueNAS handles that for you if you use an ix-volume.

Two things the TrueNAS chart already gets right, so you do not have to think about them: it sets AUTH_COOKIE_SECURE=false for plain HTTP on your LAN, and its container health check points at /api/db/health. If you write your own chart, copy both.

Adding your own settings

The install form has an Additional Environment Variables list, for names the template does not already set itself. The AI features need a key rather than a full description: LLM_API_KEY alone is enough, because LLM_PROVIDER defaults to Gemini and LLM_MODEL defaults per provider. Name them only to choose something else, and use LLM_API_URL instead of a key for an OpenAI-compatible endpoint. Single sign-on goes here too, as NEXT_PUBLIC_AUTH_PROVIDER=oidc with the OIDC settings beside it.

Proxmox VE

There is no Proxmox template. You run the container inside an LXC or a VM.

Proxmox is not an app store, so there is nothing to click. What you do is create a small guest, install Docker in it, and then follow the Docker Compose instructions. This is worth saying plainly because the Proxmox variable is where the login loop bites hardest.

A container-sized guest

An unprivileged Debian 12 LXC with 1 vCPU, 1 GB RAM and 8 GB disk is enough. The app caps its own Node heap at 384 MB. If you prefer a VM, use whatever you normally use; nothing here is Proxmox specific past this point.

Docker in an LXC

Docker inside an unprivileged LXC needs nesting and keyctl turned on. From the Proxmox host shell, with your container ID in place of 120:

pct set 120 -features nesting=1,keyctl=1
pct start 120   # pct reboot 120 if it is already running

Then inside the guest:

apt update && apt install -y ca-certificates curl
curl -fsSL https://get.docker.com | sh

Then the compose file

Use exactly the compose file from the Docker Compose tab. One line in it matters more here than anywhere else:

This is the Proxmox one

You will reach the app at http://192.168.x.x:3000, which is plain HTTP on a host that is not localhost. Set AUTH_COOKIE_SECURE to false in the compose file before you start it, so the browser keeps the session cookie: on plain HTTP a cookie marked Secure is discarded rather than stored, and the sign-in screen comes back. Set it to true the day HTTPS sits in front.

What you should see

$ curl -s 192.168.1.50:3000/api/db/health
{"status":"healthy","timestamp":"...","service":"libredb-studio"}

If you are adding the app to a Proxmox monitoring check, use that path. Most of the rest of /api/ will mislead you: some paths redirect to the login screen and read 200 from there, so the check goes green without ever reaching the app.

Your own machine, or no machine at all

Nine package channels and two hosted instances. No server required.

Try it without installing anything

Two instances are already running and open to the public. app.libredb.org runs 0.16.1, the release described here, and is wired to single sign-on, so it is also what the OIDC screen from step 01 looks like in the wild. trial.libredb.org signs you in with a password instead: admin@libredb.org / Admin!2026 for the administrator, or user@libredb.org / User!2026 for the lower-privilege account, which is the quickest way to see what the two roles actually differ on. It is on 0.15.0 today, older than these pages, so treat small differences as age rather than error. Both open on the two sample databases that ship inside the image.

One command, nothing installed

npx @libredb/studio --port 3888

If Node 24 or newer is on the machine, this is the shortest path there is. The launcher downloads the standalone archive for your platform from GitHub Releases, verifies it against the release's SHA256SUMS, and caches it under ~/.libredb-studio/0.16.1/. Later runs start straight from that cache. If the GitHub CLI is installed and signed in it also verifies the archive's signed build provenance; without it you get a warning, but an archive whose provenance is actively rejected stops the launcher.

What you should see

Downloading https://github.com/libredb/libredb-studio/releases/download/0.16.1/libredb-studio-standalone-0.16.1-darwin-arm64.tar.gz
Downloading .../SHA256SUMS
Checksum verified
Provenance verified for libredb-studio-standalone-0.16.1-darwin-arm64.tar.gz
Unpacking into ~/.libredb-studio/0.16.1/payload
Starting LibreDB Studio 0.16.1 on http://127.0.0.1:3888

The first run spends a minute downloading, then prints the address and seeds the two sample databases. It binds to 127.0.0.1, so it is yours alone until you set HOSTNAME=0.0.0.0. Open it, sign in, and the dashboard shows two healthy connections you did not have to configure. If you did not set ADMIN_PASSWORD the launcher generates one and prints it, exactly as the container does.

A package manager instead

# macOS and Linux
brew install libredb/tap/libredb-studio

# Windows
winget install LibreDB.Studio
choco install libredb-studio

# Linux
sudo snap install libredb-studio

The channel you pick decides the version you get: npm, the Homebrew tap and the Snap Store's stable channel carry 0.16.1, while winget is on 0.16.0 and Chocolatey on 0.15.0. macOS and Linux have the Homebrew tap; Windows has winget and Chocolatey; Linux also has the Snap Store, the signed FlatPark remote, and .deb and .rpm packages attached to the release. Everything on the release can be checked. Eleven assets carry their own .sha256 beside them — the .deb and .rpm packages, the AppImages, both snaps and the CycloneDX bill of materials — and a SHA256SUMS file covers the five standalone archives, which are the only ones without a checksum of their own.

The desktop application

There is a packaged desktop build as well, shipped as an AppImage and a GUI .deb for x64 and arm64. Read the platform list before you plan around it: the desktop build is Linux only today. On macOS and Windows the package channels above install the server and you reach it in a browser, which is what the Runs on Linux · macOS · Windows line on the sign-in screen is actually telling you.

Tour

The tour, one screen at a time

Every screenshot below came out of a live 0.16.1 container with a PostgreSQL database of 620 orders and a MySQL database of 900 transactions behind it. No mockups, no placeholder rows.

  1. Getting in
  2. Connecting a database
  3. Reading what is there
  4. Asking questions
  5. Changing data
  6. Understanding a schema you did not write
  7. Getting around faster
  8. Running it for other people
  9. The database agent

Getting in

Three screens before you touch a database.

01

Sign in

#

There is no sign-up step and no "create your admin account" wizard. The administrator account comes from the environment you set at install time, or from a password the app generated on first boot and printed to the log. Type it here.

The left half of this screen is worth a moment if you are deciding whether the tool is for you. It lists every engine that has a driver, and under that the twenty-six engines that connect through one of those drivers by speaking the same wire protocol. Most are not forks: CockroachDB, Materialize and RisingWave are their own engines, while Citus and TimescaleDB are PostgreSQL extensions. What this line deliberately does not carry is how far the support goes on each — that belongs to the New Connection dialog, which names the tier engine by engine.

Three counters sit under the engine list, and none of them is typed by hand: 16 database engines, 31 install channels and 2 agent modes. The last one is worth reading now, because it is the rule from step 33 printed on the login screen: plan mode drafts one statement and runs nothing, agent mode runs read-only statements and only on PostgreSQL, SQLite, DuckDB and SQL Server. The counter reads sixteen while the panel above it shows seventeen pills, and the seventeenth is marked (embedded): LibreDB's own store, a provider the app carries rather than a database you point it at. The line underneath, Runs on Linux · macOS · Windows, is the union of every operating system those channels cover between them, not a claim about any single one. The two lines under the other counters are worth the glance too: the engines one says one client, one workspace, every one of them, and the channels one names the four groups those thirty-one fall into — Containers, Kubernetes, PaaS, Packages.

One branch of this form is not in the screenshot, because this deployment does not use it. With NEXT_PUBLIC_AUTH_PROVIDER=oidc the email and password fields are replaced by a single Login with SSO button, and no administrator password is generated at all. Where you land depends on the account: an administrator arrives at the dashboard, anyone else goes straight to the editor.

Two more things this screenshot cannot show you. Below 1024 pixels the left panel is not narrower, it is gone: the card reads Sign in instead of Welcome back, the engine pills move underneath it, and the three counters collapse into a single line. And there is no forgot-password link, no reset flow and no remember-me box anywhere on this screen. If the password was the generated one you do not need any of that: the same first-run banner that printed it also names the file it was written to, auth-bootstrap.json in the data directory, and tells you that deleting that file regenerates the pair. Setting ADMIN_PASSWORD in the environment overrides it either way. A wrong password answers with the same Invalid email or password as an unknown account, on purpose; a server with no administrator password configured answers 503 and says so.

LibreDB Studio sign-in screen: engine list and three counters on the left, email and password form on the right, version v0.16.1 under it.
Sign in. The version number sits under the form, which is the quickest way to confirm what you are actually running. If the connection string on the left reads something other than mongodb:// when you look, that is not drift: it cycles through nine schemes every 2.6 seconds, and stops if your system asks for reduced motion.
02

Admins land on the dashboard

#

If you sign in with the admin role you arrive here rather than in the editor. It is a fleet view: how many connections exist, how many queries have run, how each endpoint is answering and how fast. On a fresh install it is mostly zeroes, which is the honest answer.

The Editor button top right is what you want next. A non-admin user skips this screen entirely and goes straight to the editor.

The strip across the top is the whole fleet in one line. The ring on the left divides healthy endpoints by total endpoints — five of six healthy reads 83%, with HEALTH under the number and a pulsing LIVE dot under that. The heading beside it is written from the worst endpoint and has three states: All Systems Operational, Degraded Performance if any endpoint is degraded, and Attention Required in red if any is erroring. The badges next to it do the counting — 5 healthy, 1 error, and a degraded count between them when there is one. At the right of the strip sit the signed-in account with its role, admin@libredb.org (admin), and a Refresh button.

Four counters follow: CONNECTIONS, TOTAL QUERIES, DB SIZE and TODAY, the last with the change on the day under it, ↗ +24 vs yesterday. DB SIZE is the one to read carefully. It adds up only the connections that could report a size, and the small grey (1 excluded) beside 25.91 MB is the only sign that anything is missing — an endpoint that cannot answer is left out of the sum rather than counted as zero.

Refresh measures every endpoint again, and the latency figures move each time you press it: 5ms to 8ms, 4ms to 6ms and 5ms to 9ms on one press here, while the counts stayed where they were. The page also re-reads the fleet by itself every sixty seconds, which is what LIVE means. That makes three different refresh behaviours in the admin dashboard — Overview on its own timer, Monitoring silent until you start it, Operations only when you ask. Sizes are printed in whatever unit the engine hands back, so PostgreSQL endpoints read 8727 kB and MySQL ones 0.17 MB on the same screen; only the DB SIZE counter converts the lot to MB.

Fleet Status is one card per endpoint, three to a row, with the count beside the heading — 6 endpoints here. A card carries a status dot, the connection name (truncated when it is long, Northwind (PostgreS…), the environment badge in the connection's own colour, the engine's icon in the top right, a rule along the top edge painted with the status colour, and a triple along the bottom: response time, database size and open connections on the server, 5ms · 8727 kB · 9 conn. A connection whose environment is Other gets no badge at all. Every card is a link, and all of them lead to the same place — the Monitoring section, where you still pick the connection yourself.

An endpoint that cannot be reached turns its card and its border red, drops the three numbers and prints a state word with the engine's own sentence under it: timeout, then LibreDB file is already open by another process (exclusive lock). That is the whole of it — there is nothing on the card to click, and it stays red until the connection answers again.

Key Metrics under the fleet is four tiles: QUERY SUCCESS, FLEET HEALTH and AVG RESPONSE drawn as rings, and TOTAL QUERIES as a plain number with the day's change under it. Two of the four repeat the strip above — fleet health is the same percentage as the big ring, total queries the same counter — so seeing a figure twice is the layout, not a disagreement. Query Volume (7 days) beside it stacks two series over the last seven days, Success in green and Failed in red. There is no legend, so hover a day to have them named; and because the two stack, the red line reads as that day's total rather than its failures. With no history at all the panel says No query history yet.

Recent Activity is a scrolling list of the last fifteen things that happened, and it mixes two kinds of row. Statements run from the editor show the SQL with the connection name under it. Audit lines from the database agent show the action instead — sql.query.read agent/operations/execution — with nothing under them. Every row ends with a green tick or a red cross and a relative time, 47m ago. It is the same material the Audit section keeps, cut to the most recent few.

Quick Actions at the foot of the page is three cards, each ending in Open →: Maintenance opens Operations, Security & Masking opens Security, and Real-time Monitoring opens Monitoring. The third card's own description is the only mention of alert thresholds anywhere on this page, and the tab that sets them is described later in this part.

Logout, in red beside Editor, ends the session without asking for confirmation and drops you on the login screen; the account menu in the editor holds a second copy of the same button. A logout line goes into the audit record either way. Ask for an admin address while signed out and you are redirected to the login screen with no message at all, and signing back in returns you to the address you asked for.

Admin dashboard: a 100 percent health ring, seven connections, twenty-three queries, 26.24 MB database size, and a fleet of seven endpoints.
Admin dashboard. Overview, Operations, Monitoring, Security and Audit are the five admin sections. We come back to the last three near the end of the tour.
03

The workspace

#

This is where you will spend your time, and it is three panes. On the left, your connections on top and the object tree below them. In the middle, tabbed SQL editors over a results area. Along the top of the results area, the nine ways to look at whatever the last query returned. The tree is worth a line of its own: a schema opens into folders — Tables, Views, Materialized Views, Sequences, Functions, Procedures and Triggers — each carrying a count, so an empty kind reads zero instead of being absent.

The bottom left corner always shows connection state and version. The top right of the editor pane has the monitoring link, the theme switch and the account menu. That menu is short, and it is the way back out: Admin Dashboard, which is drawn only for an admin, Monitoring, and Logout — the only place in the workspace where the session ends.

The folders in the tree are not a fixed set: each engine gets the kinds it actually has. PostgreSQL draws seven — Tables, Views, Materialized Views, Sequences, Functions, Procedures and Triggers. MySQL draws six, and they are not a subset: Tables, Views, Stored Procedures, Functions, Triggers and Events, with no Materialized Views and no Sequences. So a kind missing from the tree can mean the engine has none of them or that the engine has no such thing, and the count beside the folder is what separates the two.

The three panes are not fixed widths. The divider between the sidebar and the editor, the one between the editor and the results area under it, and the one between the editor and the agent rail on the right are all draggable, so a wide result or a long statement can take the room it needs without anything being hidden to make space for it.

Below 768 pixels that layout is not narrowed, it is replaced. The sidebar and the agent rail are gone, and a bar along the bottom of the screen shows one pane at a time: DB, Schema, SQL and Agent. The connection title at the top becomes a dropdown, and the editor toolbar collapses into a menu holding Format SQL, Copy Query, Clear, Save Query, Explain Plan, Advanced, BEGIN Transaction, Enable Sandbox, Disable Editing and Import Data, with RUN keeping a button of its own. The results bar grows a pair the wide layout does not have, Card view and Table view: card view draws each row as its own card, the first column as the heading and the rest as label and value, which is the only way a twelve-column result is readable on a phone.

The three-pane workspace with the object tree: connections above, then public with Tables, Views, Materialized Views, Sequences, Functions, Procedures and Triggers, each carrying a count.
The layout. Two sample databases ship with the image so the tree is never empty on a first run. The one open here is a PostgreSQL Northwind: seven tables, one view, six sequences, and zero of the other four kinds.

Connecting a database

The plus icon at the top of the left pane.

04

Choose an engine

#

Seventeen tiles: sixteen external engines plus LibreDB's own embedded store. Picking one changes the form underneath it, because the fields genuinely differ. Host and port for most, a file path for SQLite, DuckDB and LibreDB's own store, an auth token instead of a password for libSQL. The grid is two columns, and whether it scrolls is a question about your window rather than about the dialog: in a window 1800 pixels wide and 1150 tall all seventeen tiles are in view at once, with the grid's content and its visible height both measuring 870 pixels and nothing to scroll. Make the window short and the rest go below the fold.

Two fields above the engine grid are easy to skim past. Query Timeout (ms) takes a per-connection ceiling; leave it blank and the sixty-second default stands, which is what the hint under it says: Leave blank to use the default of 60 seconds. A value outside 1 to 2147483647 milliseconds is refused with a message saying so. The checkbox beside it, Do not read the object list on connect, stops the tree being fetched when you open the connection — The editor still works. The object panel offers a load action instead. On a database with thousands of objects that is the difference between a connection that opens at once and one that waits. If you already have a connection string, use the Paste URL shortcut at the top right of the dialog. It opens a panel headed Paste Connection URL over a box reading postgres://user:pass@host:5432/db or mongodb://..., and Parse fills the form from the URL — including switching the engine tile. The hint under the box names six schemes, Supports: postgres://, mysql://, mongodb://, redis://, oracle://, mssql://, but it reads more than it lists: couchbase, clickhouse and libsql parse as well, and a couchbase:// URL also flips the form into its Connection String mode. Hand it something it cannot read — a bare word, an ftp:// URL — and nothing happens at all: no error, no warning, the form simply stays as it was. Silence is the whole failure message.

Four fields are on every engine's form: Connection Name, Query Timeout (ms), the object-list checkbox and Environment. Below them the forms diverge, and it is worth knowing which engine asks what before you open the dialog. Most take the same four — Host & Instance, which is host and port on one row, then Username, Password and Database Name — with the port already filled in: PostgreSQL 5432, MySQL 3306, Oracle 1521, SQL Server 1433, MongoDB 27017, Couchbase 8091, Redis 6379, ClickHouse 8123, Druid 8888, Elasticsearch and OpenSearch 9200, Trino 8080, Cassandra 9042, libSQL 8080. Then the departures. SQLite, DuckDB and LibreDB take a Database File Path and nothing else — no host, no port, and neither the SSL nor the SSH panel. MongoDB adds an Authentication Database. Couchbase calls the fourth field Bucket Name. Trino adds two, Catalog Name and Schema Name. Cassandra adds Keyspace Name and Local Data Center. libSQL drops the user name and takes an Auth Token where the password would be. And three have no database field at all: Druid, Elasticsearch and OpenSearch stop at host, user and password, which is the shape of those engines rather than an omission in the form.

Seven of those fields carry a line of help under them, and between them they answer most of what you would otherwise have to look up. MongoDB's Authentication Database: The database the user was created in, usually admin. Leave empty when the credentials live in the database above. Trino's Password: Trino refuses a password over plain HTTP. Enable TLS below, or leave this empty to connect as an unauthenticated user. Trino's Catalog Name: The Trino catalog to open, such as tpch or hive. Its schemas are the level below. Trino's Schema Name: Used for unqualified table names in queries and Create Table. Leave empty to qualify names yourself. Run SHOW SCHEMAS to list the catalog's schemas. Cassandra's Keyspace Name: The keyspace to open. Tables inside it are the level below; statements can still name any keyspace in full. Cassandra's Local Data Center: Required: the Cassandra driver refuses to connect without it. A stock single-node install reports datacenter1; the server lists the ones it has if this is wrong. And libSQL's Auth Token: Turso Cloud mints this per database (turso db tokens create). A self-hosted libSQL server started without authentication takes none - leave it empty.

New Connection dialog, showing the Query Timeout field and the do-not-read-the-object-list checkbox above the environment chips and engine tiles.
New Connection. The five environment chips (PROD, STAGING, DEV, LOCAL and Other) are not decoration. Four of them put a coloured label beside the connection's name in the sidebar, so you can see at a glance which one you are about to run something against. Other is the opt-out: it stores the choice and draws no label. The label's colour is the connection's own, which the dialog sets from the environment you pick — so a seeded or hand-edited connection can carry a colour of its own while still reading DEV.
05

Test before you save

#

Scroll down and you get the fields this engine actually takes. For PostgreSQL that is Host & Instance (host and port share one row), Username, Password and Database Name, plus two collapsed panels for SSL/TLS and an SSH tunnel. Other engines differ: libSQL asks for an auth token and no user name, Trino for a catalog, Cassandra for a keyspace and a local data centre, and the file-based engines get neither panel. The tunnel is opened before the driver connects and the connection is rewritten to the local end, so it works for any engine you address by host and port. It cannot help a connection saved as a pasted URI instead — there is no host for the forward to reach.

Press Test Connection first. The app really opens the connection against the server and then reads the server's health panel, and the number it prints is how long that health read took — not how long the connection itself took to open. A pass reads Connected successfully (14ms). A failure hands you the driver's own words rather than a tidied summary — Failed to connect to PostgreSQL: password authentication failed for user "studio" — which is what you want, because the engine knows why and the dialog does not.

Four engines offer a second way to fill the form, and for two of them it is the only practical way. Above the fields on MongoDB, Couchbase, ClickHouse and libSQL sit two buttons, Host / Port and Connection String. Switch to the second and host, port, user and password disappear, replaced by Connection URI — MongoDB's placeholder reads mongodb://localhost:27017/mydb or mongodb+srv://... — and Database Name (optional override), whose placeholder says what the blank does: Extracted from URI if not provided. On Couchbase the override is Bucket Name (optional override). The other thirteen engines have no such toggle. This is the mode an Atlas or a Capella endpoint wants, and it is also the mode a pasted couchbase:// URL puts the form into by itself.

The SSL / TLS panel is collapsed, and the badge next to its heading tells you what is in force without opening it — SSL, REQUIRE, VERIFY-SYSTEM, VERIFY-CA or VERIFY-FULL. Inside, SSL Mode offers those five and explains each one where it stands. disable: Plaintext. Nothing is encrypted. require: Encrypts but verifies nothing - any certificate is accepted, including a forged one. verify-system: Encrypts and verifies the certificate chain and host name against the system trust store - no certificate to paste. Use this for a managed endpoint (Neon, Supabase, Atlas, RDS, Capella). verify-ca: Encrypts and verifies the chain against the CA certificate below. Paste one for a private CA. verify-full: Encrypts and verifies the chain against the CA certificate below, and that it names the host you typed. The fields follow the mode: disable shows none, require and verify-system show CA Certificate (PEM), and the two verifying modes add Client Certificate (PEM) and Client Private Key (PEM) — which is where a client-certificate login goes, the one thing the panel's name does not suggest it holds. For a hosted database, verify-system is the one to reach for: it verifies properly and there is nothing to paste.

The SSH Tunnel panel opens on a single checkbox, Enable SSH Tunnel, and an ON badge appears beside the heading once it is ticked. Six fields follow: SSH Host (bastion.example.com), Port, already 22, Username (ubuntu), and an Auth Method of two buttons — Password, which shows SSH Password, or Private Key, which shows Private Key (PEM) and Passphrase (optional) beneath it. When the forward cannot be made the error is the tunnel's rather than the database's, and it says so: SSH connection error: connect ECONNREFUSED 127.0.0.1:22.

Which fields are actually required is checked before anything is dialled, and the message appears in line just above the buttons. PostgreSQL, MySQL, SQL Server and MongoDB each want a database — Database name is required for PostgreSQL, and the same sentence with the engine's name for the others. Couchbase wants a bucket: Couchbase requires a bucket (use the "database" field). Cassandra wants the data centre, and explains why: Apache Cassandra requires a local data centre (localDataCenter): the driver refuses to connect without one. A stock single-node install reports datacenter1. The file-based three want a path, and their messages carry a feature that appears nowhere else in the interface: Database file path is required for SQLite (use "database" field or ":memory:" for in-memory) and Database file path is required for DuckDB (use the "database" field, or ":memory:" for an in-memory database). Typing :memory: in the path field gives you a scratch database that exists for as long as the connection does, which is the quickest way to try a query against nothing in particular. LibreDB wants a real file: LibreDB requires a file path (use the "database" field, e.g. /data/app.libredb). The remaining eight — Oracle, Redis, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino and libSQL — are not pre-checked at all. They dial straight away and hand you whatever the driver says, such as Failed to connect to Redis: Connection is closed.

The lower half of the connection dialog: the compatibility note naming eleven verified engines, then host, port, username, password and database, with SSL/TLS and SSH Tunnel below.
What the driver also serves. Above the fields, for the five drivers that have verified relatives, the app names the other engines that driver was probed against, the exact version it was probed on, and how far the support goes — partial support or query editor only where it was not everything. A closing line points at the documentation table for what each one is missing.
06

Keep several open at once

#

Connections stack in the sidebar and switching between them is one click. The explorer, the editor tabs and the results all follow the connection you have selected. Five small things live around that list. A connection can be marked a favourite, and the star does not reorder the list: the first one you mark opens a Favorites heading of its own, the favourites move under it and everything else stays under Connections, so it is a second section rather than a sort order. The order is yours to set by dragging. The pencil reopens the connection form under the heading Edit Connection, with every field of it editable — name, query timeout, environment, engine, host, credentials, SSL / TLS and SSH Tunnel — and Test Connection beside Save Changes at the foot, which is where a rotated password or a moved port goes. A connection can be duplicated, which is the quick way to point a second one at another database on the same server without retyping the credentials, though Duplicate connection does not make the copy on the spot: it opens that same form with the name already reading Retail Sales (MySQL) (copy) and host, port, user, password and database filled in, and nothing exists until you press Save Changes. And deleting one asks first, in a box headed Delete connection? whose body names what goes: Retail Sales (MySQL) (copy) will be removed. This cannot be undone. The favourite flag and the order are stored where your connections are: per user on the sqlite and postgres stores, per browser on the default local one.

The counts sit on the folders themselves — seven beside Tables here — and each table underneath carries a row count read from the engine's statistics rather than by counting, which is why it can lag reality.

Edit opens the same form under a different heading: Edit Connection, over Update your database connection parameters., with Cancel, Test Connection and Save Changes along the bottom. Two differences are worth noting. There is no Paste URL shortcut here — it exists only when the connection is new. And the engine tiles are still live, so an edit can change a connection's engine out from under it rather than making you build a new one.

A folder whose count reads zero still opens — the arrow turns and nothing is underneath it. There is no line saying it is empty, so the count in the folder label is the whole answer, and clicking one of the zeroes is not a click that failed. And not every number in the tree is the kind of row count the tables carry. Open Sequences and each of the six here reads 1, in the same place and the same shape as a table's count, because it is the same statistics field and a sequence relation holds exactly one row. It is not the sequence's current value, and not the number it will hand out next.

One deletion does not behave like the others. The two embedded samples ship as ordinary-looking connections and carry a Delete connection button, but deleting one does not remove a record — it writes the sample onto a dismissed list, and there is no control anywhere in the interface that takes it off again. On the server-side stores that list lives on the server, so switching browsers does not bring the sample back either. It is a one-click, unrecoverable loss of the thing this guide keeps telling you to try the agent on. Leave them alone unless you mean it.

When the tree cannot be read the panel says so in place of the folders, headed The object list could not be read with the engine's own reason under it — LibreDB file is already open by another process (exclusive lock). Close the other writer, or wait for its lock to be released. — and a Try again button. The reason is passed through rather than summarised, so it is usually the sentence that tells you what to fix.

The sidebar: seven connections, three of them seeded and padlocked, above the object tree with Tables and Views open and the order_totals view selected.
Seven connections, four engines. Three of them carry a padlock and the rest were added here — but the padlock marks a managed connection rather than a seeded one, which is not the same thing (see step 32). The numbers beside the tables are estimates, not counts: categories reads 8 here while the table actually holds more. Where PostgreSQL has never analysed a relation it reports no estimate at all and the app draws no number, rather than a zero. Running ANALYZE refreshes the figure.

Reading what is there

Explorer, context menu, grid.

07

Read an object's definition

#

Clicking a table name opens a tab named after the table and runs SELECT * FROM <schema>.<table> LIMIT 50 for you. The rest of the schema answers to the right button: right-click a view, a procedure, a trigger or a function and the menu offers View Source.

The definition arrives in its own read-only editor tab, and the line above it tells you where the text came from, because engines do not agree. It reads one of three things: stored by the engine as it was submitted, rebuilt by the engine from its catalog, or a structured definition rendered here as JSON. A second line says whether you are looking at a complete statement or at the body only, and if the read was cut short it says that too. Fifteen of the seventeen engines have this surface somewhere in their tree; the two without it are Apache Druid and LibreDB's own embedded store, where there is no Source tab at all. On PostgreSQL functions and procedures, Trino functions and Redis function libraries the definition can be edited and applied; everywhere else the panel says why it cannot, in words, rather than leaving a dead button.

That tab is read-only in a fuller sense than the phrase usually carries: it has no toolbar at all. There is no Format, no Copy, no RUN over it the way there is over an editor tab, so getting the definition out of there means selecting the text yourself.

A Source tab opened from the object tree, titled Source: public.order_totals, with a VIEW badge, a line saying the engine rebuilt it from its catalog, and the view body below.
A view's source. The tab is titled Source: public.order_totals. The object's name sits at the left of the header with a VIEW badge pushed to the far right of that same line, and the line underneath them says the engine rebuilt this from its catalog — which is why it goes on to warn that this is the body only and not a complete statement, and why it cannot be replaced from here.
08

Right-click a table

#

This menu is the fastest route to most of the tour, and on a table it has six entries. Generate Query drafts a statement without running it. Profile Table, Generate Code and Generate Test Data open the analyst tools. Analyze Table and Vacuum Table do not run anything from here — they open the admin Maintenance panel with this table already selected, so the operation is one deliberate click away rather than one accidental one. On a view the sixth entry is View Source and Profile Table reads Profile View. The menu only offers what the engine actually has.

The folders answer to the right button too, but only one of them does. Right-click Tables — or press the Actions for Tables button that appears when you hover the row — and the menu has a single entry, Create Table. It is the one schema change in the app you can make without writing SQL. The form asks for a table name and then takes columns one at a time through Add Column, each with a name, a type and three checkboxes, PK, NULL and UNQ, with a first row already filled in as id of type Auto-Increment, primary key and unique. A SQL Preview box under the form writes the statement as you go, and reads -- Name your table to see SQL until the table has a name, so you can see what CREATE TABLE is about to send before you press it. The other six folders and the schema node above them have no menu at all: right-clicking those does nothing.

Generate Test Data is the one entry in that table menu that can write, so it is worth knowing what the last button does before you reach it. It opens a panel headed Test Data Generator with the table's name beside it, picks a generator for each column from that column's name and type — text, integer, price and so on, with auto-increment columns struck through and left out of the statement — and writes an INSERT for 5, 10, 25, 50 or 100 rows, Regenerate giving you a fresh set of values at the same size. A line underneath counts what it built, and then there are two buttons. Copy puts the statement on the clipboard. Execute runs it against the connection you are on, and those rows are in the table when it returns.

The right-click menu on the orders table, showing six entries: Generate Query, Profile Table, Generate Code, Generate Test Data, Analyze Table and Vacuum Table.
The table menu. Five engines offer no maintenance operation, so those two entries are absent rather than broken: Druid, Elasticsearch and OpenSearch, and with them Cassandra and LibreDB's own embedded store.
09

The data grid

#

Clicking a table in the tree opens a new tab named after it, puts the statement in the editor and runs it. The grid is virtualised, so scrolling a large result stays smooth — but it is not unbounded: a result is fetched five hundred rows at a time, the bar marks one that has more still on the server, and Load More under the grid pulls the next page.

Every column header carries its type where the engine reports one. The row and column counts sit at the left of the bar under the tabs, the execution time at its right, and AUTO-LIMITED appears in that bar when the app added a limit you did not write yourself, whether or not the result ran into it. Down the left of the grid runs a sticky control column, one button per row, and it opens that row on its own as a vertical list of field and value, which is how you read a two-hundred-column result. In the same bar, WRAP lets long values fold onto several lines inside their cell instead of being cut off, which is what you want for a column holding a query, a stack trace or a JSON blob.

Two things that bar does not advertise. Every column header is a button: click it and the result sorts on that column, an arrow in the header shows the direction, and the accessible name says it out loud — quantity, integer, sorted ascending. Beside each header sits a funnel, Filter column, which opens a one-line box placeholdered Filter quantity... and keeps the rows whose value contains what you typed, case ignored. The moment a filter is on, the bar grows a counter reading 1 filter • 25 shownfilters past one — and that counter is itself the button that clears them. Both of these happen in the browser over the rows already fetched, and no statement is sent, so on a result the app auto-limited you are sorting and filtering the five hundred rows on screen rather than the table behind them.

The row button in that sticky left column opens the row in a panel headed Row #2 for the second row, with Copy JSON in its top corner and Close at the foot, so a row you want to paste into an issue does not have to be reassembled by hand. Two labels in the bar go by a second name worth recognising: WRAP reads WRAP ON once it is on, and the EXEC TIME badge carries the number with no unit attached — the milliseconds are the figure printed next to the row count at the top of the bar.

Fifty rows of the customers table in the results grid, nine columns with their types in the headers, a sticky control column down the left and an Export button carrying the row count.
50 rows in 3 ms. Column widths are narrow by default and header names truncate. Drag the borders if you need to read them.

Asking questions

The editor, five of those nine result views, and the ER diagram.

10

Write a query

#

The editor is Monaco, the same core as VS Code, with schema-aware completion for your tables and columns. Format is Alt+Shift+F, and Cmd or Ctrl with Enter runs — the selection if you have made one, otherwise the statement your cursor is sitting in, not the whole buffer. On engines that support transactions you also get BEGIN and SANDBOX buttons in the toolbar, and what they open belongs to the signed-in account rather than to the connection, so a shared connection cannot let one person roll back another's uncommitted work. Two tabs of the same account share one, deliberately: nothing in the token or the cookie identifies a tab, so the alternative would be inventing an identity the product does not have. Ownership is a lease rather than a lock, five minutes from the owner's last action, so an abandoned tab cannot hold a connection shut for everyone else. Closing a tab by accident is recoverable too: a notification names the tab it closed and offers Undo, and taking it puts the tab back in front of the neighbour it sat before, with its unsaved text intact.

The tab strip has two habits worth knowing. Double-click a tab's name and it becomes an editable box in place, with Escape putting the old name back. And the + at the right opens a new tab, tooltip and all — New Query Tab (Cmd/Ctrl+Shift+X) — naming it from a counter that only ever goes up rather than from how many tabs are open, which is why a fifth tab can open as Query 5 when there are four on screen.

BEGIN and SANDBOX deserve their own paragraph, because each one rebuilds the toolbar and shuts the other off. Pressing BEGIN answers with Transaction Started over BEGIN — all queries will run in this transaction until you COMMIT or ROLLBACK, and the button itself is replaced by three things: an amber TXN badge, a green COMMIT and a red ROLLBACK. Those two buttons are how the transaction ends, and a rollback says so — Transaction Rolled Back, All changes have been discarded. SANDBOX is the other half: its tooltip reads Playground mode: queries are auto-rolled back, the button turns green, and a band sits across the top of the editor reading Sandbox Mode — All changes will be auto-rolled back. Everything you run under that band is undone for you, which is the answer to why an UPDATE that reported success left the table exactly as it was. While a transaction is open SANDBOX is disabled, and while the sandbox is on BEGIN is, so you are in one of them or in neither.

Four more buttons on that toolbar the tour has not named. Save opens Save Query, which asks for a Name, a Description and Tags (comma separated) over a preview of the SQL it is about to keep, and answers Query Saved; that form is where everything in the Saved tab comes from. Copy puts the editor's text on the clipboard and says nothing at all, so a silent button here is not a broken one. Clear empties the editor. Lines turns the line numbers off and on, its tooltip flipping between Hide line numbers and Show line numbers.

A statement the engine refuses leaves no mark on the results area at all. The Results tab stays on its empty state, Execute a query or check history over Ready to query, and the failure arrives instead as a red notification in the bottom right corner headed Query Error, carrying the engine's own words — column c.category_name does not exist in the case I measured. That notification fades on its own. The record that does not is the red row in History, which keeps the statement and prints the error underneath it.

History is one of those nine result tabs and it keeps every run, successful or not, on every connection. The panel is headed Query History over a line counting what it is showing, and under that a search box, Search by query, connection or tab..., matching the SQL, the connection name and the tab name at once. Two switches beside it narrow the list: Active Conn against All Connections, and all against success and error. Each row carries a status icon, the time it ran, the connection and the tab it came from, the SQL, the duration and the row count, and the single button on the row puts that statement back into the editor. A failed row prints its error in red under the SQL and shows a dash where the row count would be, which is how a mistake from an hour ago is still findable.

Two buttons sit above that list. Export writes it out as CSV or JSON. Clear empties it, and the confirmation is the browser's own box rather than one the app drew — Are you sure you want to clear all history? The list holds the last five hundred executions and is kept where your saved queries and favourites are kept: per user on the sqlite and postgres stores, per browser on the default local one. That is the same store the admin side's Audit reads under its Queries tab, which is why that tab shows your statements and nobody else's.

A four-table join in the editor returning eight rows grouped by country, with orders, units and revenue columns.
A four-table join over a 620-row orders table. Nine milliseconds, eight rows, and every order counted — the orders column adds up to all 620, because this query filters nothing. Tabs are independent, so one can run while you write in another.
11

Some columns come back masked

#

The screenshot above is that same query with masking switched off. The first time I ran it the revenue column arrived as asterisks with a MASKED badge on the results bar, and clicking that badge is what produced the readable version. That is display masking doing its job: ten built-in patterns match result column names by regex, and revenue matched the financial one.

An admin can click the badge to unmask. A user cannot — unless an admin has switched Can reveal on for the user role in Security, which is a setting rather than a rule. But know the boundary: masking covers the results grid and the data profiler, and stops there. The rows arrive over the wire in the clear, and the Charts and Pivot tabs draw them unmasked — build a chart or a pivot on the revenue this step just hid and the real figures come back. Treat it as a shoulder-surfing guard, not access control.

Two more places masking shows itself. In the header of a masked column a small padlock sits beside the type, so a masked column is distinguishable from one whose values simply look odd. And inside every masked cell there is a button of its own, tooltip Reveal value (10s), which shows that one value and takes it back ten seconds later — the single-cell reveal the masking rules page grants an admin lives in the grid cell, not in a menu. Pressing the MASKED badge on the bar unmasks the whole result and the badge itself changes to MASK, which is how you put it back.

The shape of a mask belongs to the pattern that matched it, so masked does not always mean a row of asterisks. The financial pattern above gives back ***,***.** and nothing else; an email keeps the first letter of each half and the @ between them; a phone keeps its last four digits, a card its last four, a token its first four and its last four; an IP keeps the first and last octet, and a birthdate keeps the day. Password and address are the two that give nothing back at all. And a column no pattern names is not masked however sensitive it looks — credit_limit comes through in the clear beside a masked email on the same row, because the financial pattern lists salary, revenue, balance, amount and their neighbours, and not that one.

The same eight rows with the revenue column replaced by asterisks and MASKED and AUTO-LIMITED badges on the results bar.
Masked by default. Column names are matched, not values, so SELECT salary is masked and SELECT salary AS x is not.
12

Read the plan

#

The Explain tab turns the engine's execution plan into something readable. On PostgreSQL and MySQL, whose plans come back as JSON, that is a tree with timings and a bar per node, a plain summary at the top, and cache hit rate, operation count and total execution above it. Seven more engines publish a plan in a plainer shape and get the tree without the summary card: SQLite, libSQL, DuckDB, ClickHouse, Couchbase, Druid and Trino. The remaining eight publish no plan format at all, so the tab is not drawn for them: MongoDB, Redis, Cassandra, SQL Server, Oracle, Elasticsearch, OpenSearch and LibreDB's own embedded store.

The buttons on the right switch between views. A JSON plan gets four: the friendly insights view, an AI Explain narrative, the raw tree, and the engine's own raw output. A plainer plan gets three — the insights view is the one it cannot build.

The plan does not arrive on its own. Explain at the right of the editor toolbar is what fetches it — without running your statement — and switches the results strip to this tab; open the tab first and you find it empty with nothing telling you why. AI Explain waits a second time: it opens on a card describing what it would do and an Analyze with AI button, and only once that is pressed does it write its Plain Language Explanation and Performance Issues.

Visual EXPLAIN reading Query looks good, cache hit rate 100 percent, 13 operations and 2.78 ms, over an execution plan tree.
Visual EXPLAIN. AI Explain needs a model configured. Without one it stays visible but has nothing to say.
13

Chart it

#

The Charts tab takes whatever is in the grid. Eight types, an X axis and one or more Y series picked from the result columns, and an optional aggregation with date grouping by hour, day, week, month or year.

Chart configurations are saved under a name and reloaded from the same tab, so a chart you build once is not gone when you close the tab. Saved charts also collect in the Dashboard tab at the end of the results strip, where each one gets a card carrying its name, its type and the axes it was built on.

Four things around those controls. The date grouping is a control of its own, labelled Group with None, Hour, Day, Week, Month and Year, and it is drawn only when the result holds a date column at all and the chart is neither a scatter nor a histogram. Choosing Pie collapses the X and Y pickers into a single Value. Beside Save, a Saved (n) dropdown lists every chart you have kept by name and type and loads one back into the editor — type, axes, aggregation and grouping together — when you pick it, and Export writes the chart on screen out as PNG or SVG. A grey line under the panel counts what it has to work with: rows, fields, and how many of those fields are numeric. A tab with nothing to draw says Cannot Visualize Data over No data to visualize rather than showing you empty axes.

A bar chart of orders by country built from the join result, with eight chart types along the top and Save and Export buttons.
Bar, line, pie, area, scatter, histogram, stacked bar, stacked area. One series is plotted here — orders by country, with the aggregation left at none. Add revenue beside it and the bars would vanish: the two share one axis and orders is three orders of magnitude smaller. That is the argument for a second chart, not a second series.
14

Or pivot it

#

A different query, a different question. This one pulls category, country and units out of the order lines, and the Pivot tab turns those rows into a grid. Pick a row field, a column field, a value field and one of COUNT, SUM, AVG, MIN or MAX. The pivot is computed in the browser from the rows already fetched, so changing the aggregation is instant.

Generate SQL at the right turns the pivot you built by clicking into a CASE WHEN statement and drops it in the editor. It comes out with FROM your_table as a placeholder — fill in the real source and it runs. Beside it, Export writes the pivot itself out as CSV or JSON: not the rows the query returned, but the grid you arranged, aggregation and all.

A pivot table of units sold, category down the rows and country across the columns, built with SUM and carrying Generate SQL and Export buttons.
Categories against countries, summed. Eight groups, eight columns, computed client-side from the rows already on screen.
15

See the shape of the schema

#

The layers icon at the top of the sidebar opens the ER diagram. Declared foreign keys are drawn as solid edges; click a table and its edges light up and pick up a 1:N label. The layout is computed automatically, and a minimap sits in the corner for anything up to 300 tables — past that it is dropped rather than rendered as noise. The box under the panel heading filters the tables, Compact collapses the column lists, and it exports to PNG or SVG. Zoom In, Zoom Out and Fit View sit in the bottom left corner, and the × in the top right closes the panel.

The ERD Visualizer showing eight tables and seven relationships, with PNG and SVG download buttons, a Compact toggle, a table filter and a minimap.
Eight tables, seven relationships. The count includes the view, because the diagram draws whatever the schema holds rather than tables alone. Trino declares no keys anywhere — its information_schema carries neither table_constraints nor key_column_usage — so there is nothing to draw a solid edge from. The diagram says so in the panel and falls back to dashed, guessed edges wherever a customer_id names a customers. The dashes and the notice are how you tell a guess from a declaration.

Changing data

Editing rows, and moving data in and out.

16

Edit a cell in the grid

#

Press EDIT in the toolbar to arm inline editing — its tooltip reads Enable inline data editing — then double-click any cell. The cell becomes an input. Enter commits it to a pending list, Escape abandons it.

Inline editing works out which table to write to from the statement that fetched the rows on screen, read under the connection's own dialect. The tab name is not consulted, so renaming a tab cannot send the save somewhere else. A query whose rows have no single table — a join, a comma-separated FROM, a subquery in FROM or in the select list, a CTE, a set operation — is refused with the reason rather than guessed at, and editing the SQL by hand is always available. Click the table you mean to edit and the statement that opens is the plainest case there is.

A cell in the sku column open as an editable input inside the results grid, with the EDIT button armed in the toolbar above.
Double-click to edit. The control is hidden entirely on engines where a single-table row update cannot be aimed safely — sometimes because the SQL has no such statement, sometimes because the engine guarantees no key the app could aim at — rather than offered and then failing. LibreDB's own embedded store hides it too.
17

Nothing is written until you say so

#

Committed cells turn amber and a change counter appears on the results bar with two buttons beside it, a green Apply changes and a red Discard changes. Edit as many cells as you like; they queue up. Two details decide whether a cell joins that queue at all. Typing the value back exactly as it was drops the entry rather than queueing a no-op, so a cell you opened and closed unchanged leaves no trace. And the save needs a key to write against: it looks for the first column called id or ending in _id, and without one it stops with No primary key column detected and tells you to edit the SQL by hand. That search is a guess, and the guess can be wrong in a way that costs you: on a grid showing category_id it will happily aim at that, and a single cell edit then rewrites every row sharing that value — measured here, one edit changed fifteen rows and the app still reported one statement accepted. Put the table's real key in your SELECT before you edit. Apply changes writes the queue; Discard changes throws it away without a word — no confirmation, no notification, and the amber is simply gone.

A save re-runs the statement that fetched the rows, so the grid shows the committed state without your pressing RUN. Do not be surprised if the row you edited is no longer in the first fifty: PostgreSQL writes an updated row to the end of the heap, so an unordered LIMIT 50 can leave it out entirely. An ORDER BY keeps it where you left it.

The products grid with one amber edited cell reading SKU-0004-REVISED and a 1 change counter on the results bar.
One pending change. Each row goes out as its own request rather than one transaction, so if a row fails the rows before it are already written: save in batches you can check. The message counts what could not be confirmed rather than naming the row. If every row was refused your edits stay on screen; if some were written the edits are cleared, and the refreshed grid is where you see which row kept its old value. A grid save is also outside the admin audit trail, which covers authentication, admin actions and agent runs.
The products grid straight after a save: fifty rows still on screen and SKU-0004-REVISED sitting where the edited cell was.
Saved, and the grid is already showing it. SKU-0004-REVISED sits where the edited cell was, the fifty rows are still on screen, and none of that needed a second RUN. A short Changes Applied message names how many statements the engine accepted and then fades on its own.
The same grid over a join between products and categories: the edited cell is still amber and the 1 change counter is still on the results bar after the save was refused.
Refused, and the edit is still there. These rows come from a join, so no single table owns them. The save stops before it sends anything, says Cannot Apply Changes with the reason, and leaves the pending change exactly where it was — the counter still reads 1 change. Rewrite the query, or edit the SQL by hand.
18

Export what is on screen

#

Six entries, then the same six again. The formats are CSV, CSV with semicolons, CSV with tabs, JSON, SQL INSERT statements, and DDL as a CREATE TABLE. The second half of the menu is that list over again: under To clipboard every one of those formats is offered as a copy rather than a file, which is the quickest way to get a result into a message or a spreadsheet without a download folder in between. The SQL and DDL forms take the table name from the tab — inline editing reads the statement instead — so a tab named after a different table produces a statement aimed at that one.

If masking is on, the export carries the masked values, not the real ones. The menu tells you how many rows it is about to write before you commit to it, and when the grid holds only part of a larger result it says so on a second line: the rest are still on the server, and have to be loaded before an export can include them.

The Export menu open: a note reading Writes all 50 rows, then CSV, CSV with semicolons, CSV with tabs, JSON, SQL INSERT and DDL, and each of them a second time under To clipboard.
Export. "Writes all 50 rows" is the current result set, not the whole table.
19

Import a file

#

IMPORT opens a four-step wizard: upload, preview, configure, import. Drop a CSV or a JSON file and it parses it immediately and shows you what it read, so you catch a wrong delimiter before anything touches the database — and if the delimiter is the thing that is wrong, the preview step lets you say so: comma, semicolon or tab, beside a switch for whether the first row is a header.

The import wizard at the Preview step showing three rows and two columns parsed from a CSV, with a header-row switch and a delimiter choice of comma, semicolon or tab.
Step two, preview. Three rows, two columns, read straight from the file.

Configure lets you target an existing table or create a new one, and maps each source column to a target column. Then it shows you the SQL it is about to run — a preview, truncated past three thousand characters, and always an append: nothing checks whether the rows are already there.

Two buttons the four steps do not name. The preview step carries Reset, which drops the file and puts you back at the upload box — the way out when the parse is wrong rather than the delimiter. And the fourth step is headed Ready to Import over a line saying exactly what is about to happen, 3 rows into public.categories, with the dialect it generated for on a badge beside it. Under the statement, Copy SQL takes the text and Execute Import is the button that writes.

The import wizard at its final step, reading ready to import three rows into public.categories above the generated INSERT INTO statement, with Copy SQL and Execute Import buttons.
Step four, the statement itself. You can copy the SQL and run it yourself instead. In this case it added three rows to a table that had eight.

Understanding a schema you did not write

Five tools that read a schema and hand you something.

20

Profile a table

#

Profile Table from the right-click menu gives you per-column statistics in one pass: distinct count, null percentage, minimum, maximum and sample values. Three cards across the top hold the totals it computed while it was there — total rows, how many columns, and the average null percentage across all of them. This is how you find the column that is mostly null before you build a report on it. Read the minimum and maximum with care on numeric columns: they are computed as a text comparison, so on a credit limit running from 2,000 to 14,000 the profiler reports 11000.00 and 9500.00 — the first and last by alphabet, not by size. Under the column list, an AI Analysis section asks a model to describe the table and prints what comes back as it arrives: purpose, key columns, relationships and usage notes. What it is given is the profile above it and the schema the sidebar already loaded, not a second read of the data, and the section is absent where no model answers.

The Data Profiler for public.customers: 96 rows, 9 columns, 0 percent average nulls, an Export button, and a per-column profile with distinct counts, null percentages, minimum and maximum, and sample values.
Ninety-six rows, nine columns, no nulls. The panel also exports what it computed, as CSV or JSON — the only way to keep a profile, since the panel recomputes from scratch each time you open it. Masking reaches here too: on this table the email and phone columns come back masked in the samples and in the minimum and maximum, which is worth knowing before you export one.
21

Turn a table into code

#

Generate Code reads the schema the sidebar already loaded and writes a type for it. TypeScript interfaces, Zod schemas, Prisma models, Go structs, Python dataclasses and Java POJOs. Nullability comes from the real column definitions, though how each language expresses it differs: Prisma marks the field optional, TypeScript and Zod mark the value nullable while the field stays required, and the Java POJO does not carry nullability at all.

The Code Generator showing a TypeScript interface generated from public.customers, nine columns, with a Copy button.
Nine columns, one interface. The dropdown at the top left switches the output format; Copy takes it to the clipboard.
22

Generate the documentation nobody wrote

#

The Docs tab builds a searchable data dictionary from the schema the sidebar loaded, not from a fresh read: every table, every column, type, primary key and nullability. Export MD gives you a Markdown file for your repository. AI Describe adds prose descriptions if a model is configured.

The Database Docs tab reading eight tables, with AI Describe and Export MD buttons and a search box above a table reference listing column, type, primary key and nullability.
Eight tables, generated on the spot. The count includes the view, because the docs cover what the schema holds rather than tables alone. It is built from the schema the sidebar loaded, not from a fresh read, so it is as current as that tree is — reload the page before you trust it against a database someone else has been changing.
23

Compare two versions of a schema

#

The Diff tab takes a source and a target. Press Snapshot to freeze the current schema under a label, change the database, then compare. The snapshot is read from the database at the moment you take it, so it is the schema as it stands then and not a copy the panel was already holding. To re-read Current Schema without leaving the tab, press Refresh in the panel header. You can also compare two live connections against each other, which is how you find out what staging has that production does not.

The Target list comes in three groups and the third is the one to know about: Current Schema at the top, then every snapshot you have taken with its label and the moment it was taken, and then, under a heading reading Fetch from connection, every connection you have saved. That last group is how the comparison between two live connections is actually made. Until both sides are chosen the panel says Select source and target to compare schemas over Take a snapshot first, then compare with the current schema, and the snapshots are listed again down the right under Timeline with their label, their timestamp and how many tables each one froze. Once a comparison is on screen, SQL Migration replaces the diff with the generated DDL and turns itself into Diff View, which is the way back.

The summary line counts added, removed and modified tables; clicking one shows the column-level difference.

Current Schema is read from the connection when the Diff tab opens, not from whatever the sidebar happened to load earlier. It is read again whenever you take a snapshot, and whenever you pick the live database as the other side of the comparison; Refresh in the panel header asks for it whenever you want it. So a DDL change shows up without leaving the tab and coming back, and a snapshot taken after one is the schema as it stands, not as it stood when the tab opened.

Schema Diff comparing the current schema against the Before release 2.1 snapshot, reading 0 added, 1 removed and 7 modified, with order_totals marked Removed above seven tables marked Modified.
One removed, seven modified. The view is what went; the seven tables differ only in constraints and one dropped column. Snapshots are kept with their timestamp and label, so you can compare against any point you froze.
24

And get the migration for it

#

SQL Migration turns that diff into runnable DDL with the dialect named in a header comment. It generates for PostgreSQL, MySQL, SQLite, Oracle and SQL Server, plus ClickHouse column modifications. It wraps the statements in a transaction only where that is meaningful: four dialects get a wrapper and the other thirteen do not. PostgreSQL, MySQL and DuckDB get BEGIN;; SQL Server gets BEGIN TRANSACTION;. SQLite and Oracle are both among the thirteen, so do not go looking for a BEGIN; in either.

Where an engine's SQL genuinely has no column-modification syntax, the generated file says so in a comment instead of emitting DDL the engine would reject.

The generated migration SQL: a header naming the dialect postgres and the counts 0 added, 1 removed, 7 modified, then BEGIN, a DROP TABLE for the removed view and ALTER TABLE statements for the changed constraints.
Generated migration. Read it before you run it. It is a starting point, not a review.

Getting around faster

Three small things you will use constantly.

25

The command palette

#

Cmd-K or Ctrl-K. It searches five groups in one box: actions, connections, tables, saved queries and recent queries. Run Query, Format Query, Save Current Query, Ask the agent about this query, New Connection, Health Dashboard, Monitoring, Schema Diagram (ERD) and Logout are all one keystroke away. The connection you are on is marked Active, each table carries its column and row counts, and each recent query carries the time it took to run. The two query groups are capped at ten entries each, so the palette reaches what you touched last rather than the whole list. One of the actions is worth singling out: Keyboard Shortcuts, bound to ?, opens a sheet of every binding the app has — the palette, run, format and new tab among them, and a whole section for moving around the object tree with the arrow keys. The sheet is read from the app's own register rather than written by hand, so it cannot drift away from what the keys actually do.

The palette's own placeholder names four categories — Search tables, connections, queries, actions... — and the panel has five. Actions lists the ten, with their keys beside the two that have any: Cmd/Ctrl+Enter on Run Query and ? on Keyboard Shortcuts. Connections lists yours with an Active badge on the one you are on. Tables carries each table's shape beside its name, 8 cols / 620 rows, and a view gets 4 cols alone, because a view has no row count to give. Saved Queries shows the name over the first line of the SQL. And Recent Queries, the fifth, shows that first line with how long the query took — 7ms, 10ms — which makes the palette a faster way back to a slow query than the history panel. Typing filters all five at once. One of the ten actions also has a button of its own: Ask about this query, in the editor's header beside the connection name, which is the same thing as the palette's Ask the agent about this query.

The shortcuts sheet itself is six sections and thirteen bindings, and it is short enough to give in full. General: Cmd/Ctrl+K opens the command palette, ? opens this sheet. Query editor: Cmd/Ctrl+Enter runs the current query, Alt+Shift+F formats it. Tabs: Cmd/Ctrl+Shift+X opens a new query tab, Left / Right arrow moves focus between tabs, Home / End jumps to the first or last. Data profiler: Escape closes it. Object tree: Up / Down arrow moves focus between rows, Left / Right arrow collapses or expands the focused row, Home / End jumps to the first or last row, Enter / Space opens the focused row, and Shift+F10 or the Menu key opens its context menu — the one binding that reaches a menu you would otherwise need the right mouse button for. A single Close button shuts the sheet.

The Saved panel those queries come from does more than export and import. A Search saved queries... box filters the cards; each card carries the name, the description, the SQL, the engine it was written against, its tags and its date; and hovering one brings up Edit and Delete for that entry. Know what clicking the name does before you click it: the saved SQL goes straight into the editor of the tab you are on, over whatever was there, with no new tab and no question asked. Deleting is the one confirmation in the workspace the app did not draw itself — the browser's own box asks Are you sure you want to delete this saved query? — and once the last card is gone the panel, search box and all, reads No saved queries found.

The command palette open over the workspace, listing actions including Run Query, Format Query, Save Current Query and Keyboard Shortcuts with its question-mark shortcut.
The command palette. Saved queries are searchable from here, and the Saved panel they come from carries a pair of buttons: Export all writes the lot to a JSON file, and Import JSON reads one back. It validates the whole file before it writes anything, so a malformed export leaves your list untouched and says so. Entries whose id already exists are skipped rather than overwritten, and the notification names how many it added and which ids it passed over.
26

Light theme, and a second engine

#

The theme control in the top bar switches between light and dark and the choice sticks. Here is the same workspace in light, this time on the MySQL connection rather than the PostgreSQL one, so you can see how little changes when the engine does. Little, not nothing, and the tree is where you see it: the folder set is the engine's, not a fixed list. PostgreSQL offers Materialized Views and Sequences; MySQL drops both and offers Stored Procedures and Events instead. An engine that groups keys rather than tables changes the sidebar more than that again.

The same workspace in light theme throughout — sidebar, top bar, results grid and agent rail all painted light — connected to MySQL, showing a grouped query across six cities and a tree whose folders are the MySQL ones: Stored Procedures and Events instead of Materialized Views and Sequences.
MySQL, light theme. The engine name and environment sit under the connection title at the top, which is the fastest way to be sure which database you are about to change.

Running it for other people

The five admin sections, and how users actually get created.

27

Live monitoring

#

The Monitoring button in the editor top bar opens a full page of its own at /monitoring, with a Back button where the admin title would be. It carries its own connection picker, so the connection you are watching is chosen here rather than in the editor, and seven tabs: Overview, Performance, Queries, Sessions, Tables, Storage and Pool. It opens on Manual — nothing refreshes until you press Start auto-refresh, at which point the badge reads Auto and the button becomes Pause auto-refresh. The interval list sits beside it whether auto-refresh is on or off and starts at 30s; the five choices are 5, 10, 15, 30 and 60 seconds. A Last: 2:06:08 PM stamp says when the figures were read, and Refresh now reads them once more without turning auto-refresh on. The tiles are colour-coded against thresholds where a threshold exists — those come from Security, further down — and size and counts are reported without one.

Panels an engine cannot answer say so rather than showing a zero. That is a deliberate choice and it shows up a lot on the wire-compatible engines.

The same seven tabs are also the Monitoring section of the admin dashboard, at /admin/monitoring. The panels, the top strip and the connection picker are identical there; the only difference is the frame around them, the admin title and its five section tabs instead of Back. Four doors reach the same panels, then: the editor's Monitoring button, the Monitoring entry in the account menu, Monitoring in the command palette, and the admin section. Whichever you use, the connection you chose last is the one that loads, and that choice is shared with the Operations section and with the editor.

The monitoring page carries a connection dropdown of its own, next to Back, Manual, the time of the last read and the interval, so moving from one database's dashboard to another's does not mean going back to the editor to change the selection there. It opens on whichever connection the editor had selected. And where the connection cannot be opened the seven tabs are not drawn at all: the page says Connection Error, prints what the driver said, and offers Try Again.

Overview opens with the server's own version string and how long it has been up — PostgreSQL 16.15 on aarch64-unknown-linux-musl, 4d 9h 24m — and the server's clock beside them. Four tiles follow: Connections as used over maximum with a bar and a percentage, 4/100 and 4% used; DB Size over Total storage; Cache Hit with a bar and a word; and Tables with the index count under it. Under those sit a Performance panel repeating buffer pool, deadlocks and checkpoint time, and a Quick Stats panel counting three things the other tabs list: Listed slow queries, Active of listed sessions and Idle of listed sessions. The same layout on MySQL reads MySQL 8.4.11 and 12/151 connections, publishes a buffer pool figure where PostgreSQL has none, and publishes no checkpoint time or deadlock count where PostgreSQL does.

Performance is three tiles across the top — Cache Hit, Buffer and Deadlocks — each with a word under the figure, and on the first two a fixed reference printed at the right, 95%+ and Cache. Three trend strips, Cache Hit Trend, Buffer Pool Trend and Deadlock Trend, appear between the tiles and the panels only once the page holds two readings or more, so with auto-refresh off you never see them. Checkpoint Stats shows write and sync time with a line about what it costs. Tips is narrower than it looks: it raises Low Cache Hit under 90 percent and Deadlocks above zero, and says Performing well! whenever neither is true. It never reads the buffer pool, which is why a MySQL connection can show a buffer tile marked Poor and a green Performing well! on the same screen.

Queries has two tiles, Avg of listed queries and Listed queries over 1s, and the word listed is doing real work: both summarise the rows in the table below them and nothing else. That table is Slowest Queries, five columns — Query, Calls, Total, Avg, Rows — with the last four sortable from their headers and the statement truncated until you hover it. The panel wants pg_stat_statements, and says so in its own empty state: Enable pg_stat_statements extension to see query stats. Without the extension you get what the server can see instead, which on my run was the application's own two monitoring statements, one call each, no rows, and durations printed below zero — -2.38ms and -2.69ms, averaging to -2.54ms in the tile above. A reading too short to measure comes back negative rather than as nothing, here and in the session lists.

Sessions puts the four counters in full-size tiles — Active, Idle, In TX, Wait — over a table with seven columns: PID, User, State, Query, Time, Wait and Act. Wait names the wait event the engine reports, Client for instance, or a dash. That is one column more than the same list in Operations, and the skull button in Act is on show here rather than waiting for a hover. It is the same button, with the same confirmation and the same consequence, described under Operations below.

Tables counts three things — Tables with the total row count, Size, and Vacuum with how many tables want one — then lists them in Table Statistics: Table, Rows, Size, Index, Bloat, Vacuum, Act. The name cell carries the schema under it, the rows cell the dead rows under the live ones, the bloat cell a badge that turns red when it is high, and the vacuum cell the date of the last one, Never, or a dash on an engine that does not vacuum. A Search... box filters the list and leaves the three tiles alone; no match gives No tables found. The last column is the part worth knowing: Analyze Table, Vacuum Table and Reindex Table sit on every row, so this dashboard is not read-only — it starts the same maintenance the Operations section does.

Storage is four tiles — DB Size, Tables, Indexes and WAL, the middle two with their share of the database — over three panels. Storage Breakdown draws tables, indexes and a third bar named Other (unattributed) for everything else; on this Northwind that third bar was the biggest of the three at 7.9 MB of an 8.52 MB database, which is worth expecting rather than being surprised by. Tablespaces lists Name, Location, Size and Usagepg_default and WAL on PostgreSQL, the data directory and ibdata1:12M:autoextend on MySQL. Largest Tables ranks them by size with each one's share of the database.

Pool is the pool the application keeps on its own side rather than anything the server reports: Total against the maximum pool size, Active with the percentage in use, Idle and Waiting. It has a refresh button of its own in the panel header. A driver that publishes no pool figures reads Pool statistics not available for this provider with all four tiles at N/A.

There are two ways a panel refuses, and the wording tells you which. When the engine's error looks like something it simply does not have, the panel says This engine does not publish this.; anything else says This database could not answer this panel. Both print the engine's own sentence underneath, verbatim — on MySQL the Slowest Queries panel showed SELECT command denied to user 'studio'@'172.28.0.5' for table 'events_statements_summary_by_digest', database user and client address included. Tiles over a refused panel read N/A with Not measured under them.

The words on the tiles are a short list, and they do not all come from the same place. Excellent, Good, Fair and Poor grade cache hit and buffer pool on a fixed scale — and it is not the same scale on both tabs, since Overview calls 90 percent and up Excellent while Performance keeps that word for 95 and up. Healthy and None detected are the deadlock tile at zero, Attention and Review queries the same tile above zero, and Not measured means the engine publishes no such figure. What the Thresholds tab under Security changes is the colour: the border around a tile is painted from the warning and critical values set there, not from the word inside it.

A panel failing is not the same as the connection failing. Pick a connection the server cannot open and the seven tabs are never drawn at all: the page becomes a single Connection Error with the engine's sentence under it and a Try Again button, and the stamp at the top reads Last: Never. Because the connection choice is remembered across sections, opening /admin/monitoring cold can land you on that screen for a connection you did not pick this time.

Monitoring for the PostgreSQL connection: PostgreSQL 16.15, 8 of 100 connections, 8.37 MB, a 99.9 percent cache hit rate, seven tables and thirteen indexes.
PostgreSQL 16.15, and how long the server has been up. Cache hit at 99.9 percent is labelled Excellent; "Not measured" and "N/A" mean the engine publishes no such figure.
28

Maintenance, with the warning attached

#

The Operations section runs the maintenance the selected engine actually supports. Each operation is a card with three parts, and they do not all say the same thing: the button in the top right carries the engine's own wording — Run Analyze, Run Vacuum, Run Reindex, Run Optimize — while the title under it is fixed whatever the engine is, Update Statistics, Reclaim Space, Rebuild Indexes, Optimize Tables, and the sentence under that names the statement it will run. So Update Statistics is not SQL Server's word for it: a PostgreSQL connection and a MySQL one both show that same title over their own button. Beside the cards, a live session list with six columns: PID, USER, STATE, QUERY, TIME and ACT.

The connection the section works on is chosen from the picker at the top, and that picker deserves a warning. Entries read name and type — Northwind (PostgreSQL) (postgres) — with no environment label, so two connections carrying the same name sit in the list indistinguishable from each other, and the closed button truncates the text before the type is fully spelled out. This is the screen that runs VACUUM, REINDEX and session kills, so confirm which copy you are on from the sidebar or the fleet cards before you press anything. Nothing here refreshes on its own either: the Refresh button beside the picker, or running an operation, is what re-reads the tables and the sessions.

Which cards you get is the engine's answer rather than a fixed set. PostgreSQL offers three — Run Analyze, Run Vacuum and Run Reindex — and MySQL two, where the second is Run Optimize and there is no vacuum or reindex at all. The red Warning card, the one telling you these are resource-intensive, takes whatever slot is left over, so it moves along the row as the count changes. The description under each title names the statement that will run and is rewritten per engine: PostgreSQL's reindex card says it runs REINDEX DATABASE, SQLite's says it runs bare REINDEX over the database file.

The Tables (7) panel under the cards lists every table as schema, dot, table name, with 1,806 rows - 136 kB under it, scrolling inside its own box. A table whose dead-row share is over ten percent carries a yellow badge, 25% bloat, and that badge is the only prompt on this screen to vacuum anything in particular. The Filter... box at the right narrows the list but not the count in the heading, which goes on showing the full number; no match gives No tables found. The same table read on the Monitoring dashboard rounds the badge differently, 25 percent here against 24.5 percent there.

Hover a row and three buttons appear at its right — and this set is not the set above. PostgreSQL gives Analyze Table, Vacuum Table and Reindex Table. MySQL gives Analyze Table, Optimize Table and Check Table, and Check Table has no card of its own anywhere. SQLite gives only Analyze Table and Reindex Table, even though its Run Vacuum card is right there above. This row, not a pre-filled form, is where the table menu's Analyze Table and Vacuum Table entries bring you: the panel opens, and you still pick the table here.

The Sessions panel puts four counters over the list — ACTIVE, IDLE, IN TX and WAIT, the last turning orange above zero. They are filled from PostgreSQL's state words, and MySQL's Sleep, Execute and Query map to none of the four, so a MySQL connection can read Sessions (6) with six rows under it and all four counters at zero. The list itself is right; the counters are what is empty. In the TIME column a long-running session goes red, a session with nothing to measure reads N/A, and a reading too short to measure can come back negative, -0.002886s.

The last column, ACT, holds the one destructive control in the application: a skull button that appears when you hover a row and ends that database session. It opens a confirmation headed Terminate Session? naming the session, its user and its state, and then saying plainly that the action "will forcefully end the connection and may cause data loss if the session has uncommitted transactions". Cancel holds the focus; Terminate is the other button. Confirm and the count in the panel heading drops by one, a notification reads Session 366 terminated successfully, and the audit log gains a KILL line. The same button, dialog and consequence are on the Monitoring dashboard's Sessions tab.

Every operation answers twice. The button spins while it runs, a notification appears bottom right when it is done — ANALYZE: OK — and a panel called Operation Log (this session) appears at the foot of the page with one line per run: the time, a badge for the operation (ANALYZE, KILL), what it was aimed at (all for the whole database, the table name for one table, PID:366 for a session), a tick and how long it took, 48ms. The heading means what it says — reload the page and the log is empty again. The copy that survives is in the Audit section.

On an engine with no maintenance at all — Druid, Elasticsearch, OpenSearch, Cassandra and LibreDB's own embedded store — the page does not grey anything out; it draws less. Global Operations, its cards, the red Warning with them and the whole Tables panel are simply absent. Only the session panel is left, and it goes on showing the sessions of the connection you were looking at before, with nothing on screen to say they are stale. Read the name in the picker before you believe that list.

The Operations page with Run Analyze, Run Vacuum and Run Reindex, a resource warning, and the seven tables listed with their sizes.
Operations. The red panel telling you these are resource-intensive and to avoid peak hours is part of the page, not something I added.
29

Masking rules

#

Security has three tabs — Data Masking, Access and Thresholds — and opens on the first. Data Masking is one panel, Data Masking Settings: a global switch reading Enable Data Masking Globally, a row of per-role permissions, and the ten built-in patterns with the column names each one matches. Email, password, SSN, credit card, phone, token, address, IP, birthdate and financial are all there, each with a switch of its own, a builtin badge, a badge naming its mask type and a pencil that opens it for editing; Add Pattern adds one of your own.

Note the role grid. It is four switches rather than a fixed rule: Can toggle and Can reveal on an Admin row, the same pair on a User row. Out of the box the admin pair is on and the user pair is off, so an admin can turn masking off and reveal a single cell for ten seconds and a user can do neither. But nothing here is disabled or greyed out. An admin who switches the user pair on has handed that same reveal to every account with the user role, and the only record of having done it is a Masking line in the audit log.

A pattern row is four things and only the first is obvious. There is a switch, so a pattern can be turned off without being deleted; a builtin badge; a second badge naming how the value is rewritten — email, full, ssn, card, phone, partial, ip, financial, date — and a pencil at the right. The pencil works on the shipped patterns too: the same dialog opens under Edit Masking Pattern, and the column list of a built-in can be rewritten like any other. The list scrolls inside its own box.

Add Pattern opens Add Masking Pattern, and it is more than a regex box. A Start from a preset row offers Email, SSN, Credit Card and Phone as starting points, with a line asking you to review the columns before saving. Then a Name, a Mask Type list of ten — email, phone, card, ssn, full, partial, ip, date, financial, custom — and a Column Patterns (one per line) box whose help line is the rule to remember: Each line is matched against column names (case-insensitive). Supports regex. One line is one pattern, and a live Preview: under the box shows the mask before you commit to it.

At the foot of the tab a Preview panel runs five sample values through the rules as they currently stand, original struck through beside the result: john.doe@example.com becomes j*******@e**********, 123-45-6789 becomes ***-**-6789, 4111111111111234 becomes ****-****-****-1234. Two buttons sit under it. Save Config writes the tab — nothing you change here takes effect until you press it, and on this tab it stays enabled whether you changed anything or not. Reset Defaults puts the ten shipped patterns back.

The third tab, Thresholds, is the one that decides when the monitoring tiles turn colour. Its single panel, Monitoring Thresholds, states its own purpose: Configure warning and critical thresholds for monitoring alerts. These values are used by the monitoring dashboard to trigger visual alerts. Four measures, each with a Warning slider in orange and a Critical one in red, and a label at the right saying which way the alarm runs. Cache Hit Ratio is ALERT WHEN BELOW, at 90 and 80 percent. Connection Usage is ALERT WHEN ABOVE, at 70 and 90 percent. Deadlocks is ALERT WHEN ABOVE, at 1 and 5, on a scale that stops at 20. Buffer Pool Usage is ALERT WHEN ABOVE, at 85 and 95 percent. Those are the values it ships with.

The sliders move a step at a time and the number beside each one updates as you drag. Save Config on this tab is disabled until you actually change something — the opposite of the Data Masking tab — and it stays painted blue while disabled, so it looks pressable when it is not. Leave the tab without saving and the change is dropped without a word: I moved a warning from 90 to 91, reloaded, and it read 90 again. Saving either tab is on the record, since Masking and Thresholds are both types the audit log can be filtered by.

Data Masking Settings with the global toggle, a role permission grid for Admin and User, and the built-in patterns with the column names each one matches.
Ten built-in patterns. Where the rules live follows STORAGE_PROVIDER: on the default local they sit in that one browser, and on sqlite or postgres — which is how this deployment runs — they are kept on the server and shared by everyone who signs in.
30

Roles, and where users come from

#

This is the screen that answers the question most people ask second. There are two roles, admin and user, and there is no user management UI. Accounts come from environment variables, or from your identity provider.

For local auth that means ADMIN_EMAIL and ADMIN_PASSWORD for the administrator, and an optional USER_EMAIL and USER_PASSWORD for a lower-privilege account that exists only if you set it. For a team, set NEXT_PUBLIC_AUTH_PROVIDER=oidc and point it at any OIDC provider: Auth0, Keycloak, Okta, Entra ID, Zitadel, Google. Authorization Code with PKCE, and OIDC_ROLE_CLAIM maps a claim to a role with dot notation for nested claims, such as realm_access.roles.

The screen itself is the Access tab of the Security section, and there is nothing to set on it — it reports the configuration it found. Security & Access lists four rows: Authentication as Environment Variable (RBAC), API Security as JWT / HTTP-only Cookie, and Admin Access and User Access each with an Enabled badge — the second reads enabled here because this deployment defines the optional second account. Connection Security lists three more: SSL/TLS and SSH Tunnel as Supported, and Data Masking as Configurable, which is the tab next door.

The Security and Access panel reading Authentication: Environment Variable (RBAC), API Security: JWT / HTTP-only Cookie, admin and user access enabled, SSL/TLS and SSH Tunnel supported, and Data Masking configurable.
Access. There is also no in-app password change. Rotating the administrator password means editing the environment and restarting.
31

The audit trail

#

Audit has three tabs, and they do not all read the same thing. Operations is the server's own record: authentication, admin actions and every statement the database agent ran, with its duration, failures marked. The row count is not on this tab — it is a column on the next one. Queries is not that — it is the query history of whoever is signed in, read back from storage, so it shows your editor's statements and nobody else's, with both duration and rows returned. Stats summarises. Both of the first two carry an Export button, writing what the filter currently shows as CSV or JSON. Under the tab strip, and only on Operations, a line says what the panel leaves out: denials the proxy records — a cross-origin request rejected at the boundary, a non-admin session reaching for /admin — never arrive in this buffer, and the libredb.audit.v1 lines on the process log are the record that holds them.

The strip above the Operations table is a type list, a search box, Refresh and Export, with Total: 79 ops and Success: 81% under it. The type list has ten entries — All Types, Maintenance, Kill Session, Masking, Thresholds, Login Success, Login Failure, Logout, Permission Denied, Rate Limited — and that list is also the plainest statement of what the application records. Search matches the action, the target and the connection name. The two figures under the strip count the whole buffer rather than the filtered view, so they do not move as you type.

The table is TIME, ACTION, TARGET, CONNECTION, USER and DURATION, with a tick or a cross in front of each row. The action is a badge and the vocabulary is mixed on purpose: login, logout and denied from the application, sql.query.read, db.schema.read and db.operations.read from the database agent, ANALYZE and KILL from the maintenance screens. A maintenance line reads ANALYZE · all · Retail Sales (MySQL) · admin@libredb.org · 31ms; a terminated session reads KILL with the PID as its target. Agent lines leave CONNECTION as a dash and put agent:admin in USER.

The denied rows are worth reading against the disclosure above them, because at a glance they look like a contradiction. They are not. What lands here is the application turning a request down — POST /api/db/health from anonymous, marked with a red cross. What the line is about is the denials recorded in front of the application, at the proxy, and those never reach this buffer at all. Two gates, and only the inner one keeps its record here.

Export is a small menu, Export as CSV and Export as JSON, and the file holds more than the screen does. The CSV header is fourteen columns — Timestamp, Type, Action, Target, Connection, User, Result, Duration (ms), Details, IP, Reason, Bucket, Correlation ID, ID — so the client address, the rate-limit bucket and the correlation id that appear nowhere in the table come out with the file. It saves as audit_operations_<timestamp>.csv, and the Queries tab writes query_history_<timestamp> the same way.

The Queries tab is built differently from the first. Its filter has three states — All, Success, Error — its box reads Search query... and matches the statement and the connection name, its summary reads 105 queries · 91% success, and it carries an Export but no Refresh and no disclosure line. Its columns are TIME, QUERY, CONNECTION, DURATION and ROWS — this is where the row count lives. Nothing matching says No query history found.

Stats is four tiles and two panels, with no filter, no search and no export of its own. The tiles are Total Queries, Success Rate with a bar, Avg Duration and Failed in red. Query Activity (7 days) is one blue bar per day, a single series rather than the stacked pair on the admin overview. Most Active Connections ranks connections by how many statements each one ran, with the count, the share and a bar — Northwind (PostgreSQL) 79 (75%). It ranks by name alone, so two connections with the same name arrive as two rows you cannot tell apart, the same blind spot the Operations picker has.

Neither of the first two tabs pages. Operations asks the server for the most recent 200 events and draws all of them on one page; Queries draws the first 200 of your history. There is no next page, no load-more and no detail view — the rows are not clickable. With nothing to show, Operations says No audit events found. and, under it, Operations will appear here when maintenance tasks are run.

The Audit Queries tab: a header reading 32 queries at 91 percent success with an Export button beside it, over a list of statements each with its connection, duration and row count.
Thirty-two queries, 91 percent success. The red rows are failed statements from my own testing. Failures stay in the record, which is the point of having one.
32

Hand people their connections

#

If you are deploying this for a team, you probably do not want everyone typing database credentials. A YAML file listed in SEED_CONFIG_PATH pre-configures connections, with passwords injected from environment variables so they never sit in the file. The version: "1" line is required; a file without it is rejected.

version: "1"

connections:
  - id: "northwind-pg"
    name: "Northwind (PostgreSQL)"
    type: postgres
    host: pg
    port: 5432
    database: northwind
    user: "studio"
    password: "${PG_PASSWORD}"
    roles: ["*"]        # or ["admin"]
    managed: true       # read-only, admin controlled

Config changes are picked up within about a minute without a restart. Set managed: false instead and each user gets an editable copy of their own.

The sidebar showing three seeded connections with DEV badges and padlock icons above the locally created ones, which carry a LOCAL badge and no padlock.
A padlock means managed, not seeded. Seeded connections sit above the ones a user added themselves and the two kinds coexist, but the padlock is drawn for managed: true alone. A seed that ships managed: false — which is what the two embedded samples are — looks exactly like a user's own connection and carries Edit, Duplicate connection and Delete connection beside it. Counting padlocks is therefore not a way of counting seeds.

The database agent

Optional, off by default, and read-only in a way that is worth explaining.

33

Two modes, and what each is allowed to do

#

With no LLM_* settings at all, the agent rail does not render and nothing leaves your network. Set a provider and it appears on the right. Gemini, OpenAI, Ollama or any OpenAI-compatible endpoint; for this guide I pointed it at a local Ollama. If you are running a local model, thirty-five of them have actually been measured against all six agent surfaces.

Plan mode is toolless. It reads your schema, drafts one statement and hands it to you. It runs nothing. Agent mode runs read-only statements itself, and the badge at the top tells you the limits in force: 200 rows and 10 seconds per statement, enforced by the engine rather than by a parser.

Each mode carries a badge at the top of the rail, and each badge opens. Plan mode's reads Executes nothing it drafts over one schema read grounds it, nothing else reaches the database, and the information button beside it gives the whole claim under the heading Plan mode drafts, and never runs what it drafted: Plan mode never executes the statement it wrote, on any engine — production included. Its one reach is the schema capture that grounds it: metadata only, no data rows, and it is where the inventory in Run details came from. On PostgreSQL and SQLite that capture is itself a catalog read; on every other engine it asks the provider to describe its own schema. That last distinction is the one to keep. Plan mode is toolless, but it is not offline: it opens a connection, and on two of the engines it reads the catalog directly.

Agent mode's badge reads Reads only over 200 rows and 10 s per statement, enforced by the engine, and its box, headed Agent mode reads, under a boundary the engine enforces, says how: Agent mode runs statements it wrote itself, in a read-only session the database enforces, bounded to 200 rows and 10 seconds each. Writes and DDL are refused by the engine rather than by reading the statement. Nothing reaches your editor unless you tick the hand-over when the run opens. Two ceilings are in force that the badge does not print. A result is also capped at 256 KB, so it can be cut before the two hundredth row and the row count alone will not tell you it was. And the profile allows one execution at a time: a run never has two reads in flight, whatever the model asks for.

On an engine agent mode cannot execute on, the badge says so before you have written anything — Cannot execute on MySQL over plan mode drafts here, and the operations workflow still runs — and a blocking box repeats the reason twice, once under the badge and once above Start, headed Agent mode has no read-only statement path on MySQL: Agent mode executes only where the provider implements a database-native read-only statement path — PostgreSQL, SQLite, DuckDB and SQL Server. On MySQL a run whose workflow sends a statement is refused when it is started, before a run is opened. The operations workflow still runs here, because it sends no statement at all: it calls the curated reporting methods every provider implements. Plan mode drafts on every engine. A Switch to Plan button sits beside it. Choose Operate and the box does not go away, which reads as a contradiction of its own third sentence; it is not one. Start stays live and the run really runs — I measured one on MySQL that finished in 29 steps having used 12 of its 18 statements. There is a third form of the same box for when no connection is resolved yet, Cannot execute yet over no connection is resolved, so no engine has been established, which is about the connection rather than about the engine.

The agent rail in Agent mode, showing the Reads only badge reading 200 rows and 10 s per statement, enforced by the engine, with a Run history button beside the mode switch.
Agent mode. Plan mode drafts on every engine, which is not the same as opening on every connection: both modes need a connection the server can rebuild on its own, so on one you typed into the browser Start stays disabled in Plan mode too. Agent mode executes on PostgreSQL, SQLite, DuckDB and SQL Server, because the read-only execution profile is database-native and those are the providers that implement it. One workflow is the exception: Operate sends no statement at all, so it runs on every engine in either mode — though the blocking notice stays on screen while it does.
34

It will refuse a superuser

#

My first run failed, and the reason is worth repeating because it will happen to you. The role I had connected with was the PostgreSQL superuser. The rail names the reason itself, and it is the role rather than the engine: The database user this run would execute as was refused by the read-only execution profile, not by the engine: it holds privileges the boundary cannot contain, or it cannot ask for the plan that admits a statement. Point the connection's agent credential at a least-privilege user. The container log (docker logs) names the privileges that tripped it:

ExecutionProfileError: The agent read-only execution profile requires a
least-privilege PostgreSQL role; this role is unverified or too broad
(is_superuser, reads_server_files, writes_server_files, executes_programs).
A read-only transaction does not stop server-side file access or program
execution.

That is correct, and it is a good refusal. A read-only transaction does not stop COPY ... TO or the local-file functions. Give the agent its own role. One consequence to know before you switch your everyday connection to it: PostgreSQL only shows a table's constraints to the owner, so on this role the Docs tab lists no primary keys and the ER diagram falls back to dashed, guessed edges. Keep the owning role for the schema tools and use this one for the agent.

CREATE ROLE agent_ro LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE northwind TO agent_ro;
GRANT USAGE ON SCHEMA public TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO agent_ro;

The agent runs on the server, so it needs a connection the server can rebuild on its own. A connection you typed into the browser is not one, and Start stays disabled — in Plan mode as well as in Agent mode — with the rail naming the reason: Northwind (PostgreSQL) LOCAL cannot be rebuilt on the server: its settings live in this browser. A run re-resolves its connection there after a restart, so it can only investigate a connection the server holds too. STORAGE_PROVIDER makes no difference here, and the sentence is worth reading closely: it says "this browser" on a sqlite deployment too, where the record is in fact on the server. What decides it is whether the server can resolve the connection from an id alone, which means a seeded one. The image seeds two samples on first boot and one of them is something to try the agent on: Sample (Employees), the SQLite one. Sample (LibreDB) is refused twice over — LibreDB is not one of the four engines agent mode executes on, and the rail declines to rebuild that sample as well — so Start is disabled there in both modes. A stored connection is still not a seeded one.

The refusal prints in three places at once, which is worth expecting so you do not go hunting for a fourth. The rail states it above the run, the Run failed box states it again below, and the last line of the step list, Run did not answer, carries it a third time. Beside the run id the chip reads failed and the section heading reads OUTCOME failed. The steps that did happen are still there to read: Run setup · 4 entries, then Tool invoked — inspect_schema via sql.query.read. The schema was read. The statement never got a session.

The remedy that message names deserves a straight answer, because the app gives you no field for it. agentUser and agentPassword are real properties of a connection record — the Edit Connection dialog carries them through untouched when you save, and agentUser is one of the values the schema capture is fingerprinted on — but nothing in 0.16.1 writes them. Neither dialog has an input for them, and the seed file's schema does not accept them, so a pair added to the YAML is dropped before the connection is built. What does work is the blunter version of the same idea: seed a second connection whose own user is the least-privilege role, and point the agent at that one. Northwind (read-only role) in these screenshots is exactly that — same host, same database, a different user — and it is the connection every successful run on this page was opened on.

There is a second reason the rail can decline, and it is not about your connection at all: The server could not read its own connection configuration, so it cannot resolve a connection for a run. This is not a problem with this connection — the server log says what failed. That one means the seed file is missing or will not parse. The first clause of each message is the one to read: cannot be rebuilt is about the connection you picked, could not read its own connection configuration is about the server.

35

A run, and what it hands back

#

With the right role it works. I asked "Which country produced the most shipped revenue?" and it inspected the schema, drafted a statement, ran it, and composed an answer. Twelve steps, five statements out of a budget of thirty, and so little database time that the counter never left zero out of ninety seconds. Those two ceilings are Investigate's own: every workflow carries its own budget, and the panel says so itself while it is still waiting to learn which one — Every ceiling here is per workflow, and Automatic decides the workflow from your objective when the run opens, so the figures are stated once the run has one, and by the run's own record. A third counter sits beside those two, Repair attempts, with a ceiling of three, and that one is three everywhere. Three buttons carry the rest of the budget: What is counted warns that a figure shown here is a floor rather than the spend, Ceilings gives the limits nothing measures — for this run 10 seconds a statement, 7.5 minutes a drive, at most 36 model turns — and Report reserve keeps back the last 2 model turns and the last 20 seconds, at whichever it reaches first asking the run once to stop and report what it has established.

The run details below the answer are the whole trace: every tool call, every stored result with its row count and timing, and every statement it drafted, each with a Copy and an Apply to editor button. Nothing reaches your editor on its own. One combination goes further and asks: in Agent mode on the Analyze workflow, pressing Start opens a confirmation panel first, carrying a tick that offers to run the final answer in your editor as well — at the editor's own 500-row limit and with no time limit, on the connection the run was opened on. Untouched, it stays off. Plan mode raises no panel even on Analyze, because it executes nothing to hand over, and the other four workflows never raise it either — which is why the run on this page, opened as Investigate, went straight to work.

Every figure in that paragraph belongs to Investigate, and the ceilings differ by more than a little from workflow to workflow. Investigate and Optimize share one budget: 30 statements, 90 seconds of database time, a 7.5-minute drive and 36 model turns. Assess raises it to 45 statements, 135 seconds, 10.5 minutes and 48 turns. Operate lowers it to 18 statements, 80 seconds, 6 minutes and 20 turns. Analyze is the widest: 42 statements, 180 seconds, 15 minutes and 60 turns. Three things do not move — 10 seconds per statement, 3 repair attempts, and the report reserve of 2 turns and 20 seconds. The Ceilings box prints whichever set the run was opened under, so on an Analyze run it reads Each statement gets 10.0 s, each drive 15.0 min and at most 60 model turns. with bars beside it reading Statements 1 / 42 and Database time 0.0 / 180.0 s, and the rail's one-line summary reads 6 steps · 1/42 stmt · 0.0/180.0 s. Read a number off this guide's Investigate run and you will not find it on an Analyze one.

Two of those three boxes say more than their summaries. What is counted, whose heading is What these figures count, reads in full: Every ceiling is per drive, so a run resumed after a restart starts each of them again and these totals can read past a single drive's ceiling. What is counted comes from the run's ledger, which records less than the server charges: the schema capture's catalog reads are not itemized, and a completed read reports the engine's own elapsed time rather than the span the budget was charged. So a spend shown here is a floor, never a ceiling. On SQLite a statement over its timeout is refused once it returns, not interrupted while it runs. That last sentence is a real difference in behaviour, not a footnote: on SQLite the timeout is a verdict passed after the fact. Report reserve, headed What is kept back for the report, ends on an exception of its own: The last 2 model turns and the last 20.0 s are kept back for the report: whichever it reaches first, the run is asked once to stop and report what it has established. So a run that ends short of these figures was asked to stop rather than having given up, and its claims still cite what it read. A plan run is never asked, having no report to compose.

The confirmation panel that Analyze raises in Agent mode is worth reading line by line, because one of its sentences withdraws part of what the tick offers. It is headed START THIS RUN with a read-only badge, then: This run will open as Analyze on Northwind (PostgreSQL), which answers with a result. and Nothing runs in your editor unless you ask for it below. The tick is Also run the final answer in my editor, off by default, and under it: The run always produces its answer on its own read-only path, bounded to 200 rows and 10 seconds. Tick this and it will also put that statement in your editor and run it there — on the connection the run was opened on, at the editor's 500-row limit and with no time limit. It is the same database-enforced read-only session either way, so writes and DDL are refused by the engine rather than by reading the statement. Statements whose plan reads as expensive, or which the run measured as slow, are put in the editor without being run. So the tick is a request, not a guarantee. Two buttons close it, Start run and Cancel, and a line under them settles when the decision is made: This is decided by the request that opens the run and stays what it was: a later request cannot widen a run the server already holds. The hand-over runs under a profile of its own — 500 rows, no statement timeout, and a 64 MB result cap.

While a run is going the rail is a different surface, and it carries the one control that interrupts a run. The heading reads WORKING with running beside it, then the latest step, then an elapsed line — 2.0 s since this run's first recorded entry — and the three counters on one row: 6 / 18 statements · 0.0 / 80.0 s database time · 1 / 3 repair attempts, under a note headed Figures are a floor, not the spend. Beside the run id sits a Stop button. A run you stop is not recorded as a failure; its ending says what happened: A stop was requested before this ending: the run took no further database step, and finished what it already had in hand. The question box is gone for the duration, replaced by the objective under a label reading The objective this run was opened with:, a Investigate · Agent mode chip, and an Edit button that puts the text back in the box for the next run.

An agent run marked succeeded, answering that Turkey produced the most shipped revenue, over a run detail line reading twelve steps and five statements out of thirty.
Run answered. The run states its own verdict rather than always producing something that reads like an answer — but it states it in three vocabularies at once, and it is worth knowing all three before you go looking for one of them. The last line of the step list is Run answered or Run did not answer. The chip beside the run id is running, then succeeded or failed. The section heading above it is WORKING running, ANSWER succeeded or OUTCOME failed. And the History list uses Succeeded and Failed, with answered beside the ones that answered. Nowhere on the screen is "did not answer" a status chip.

Expanding Evidence shows the citation behind the claim: the artifact ID, the row count, and the exact SQL that produced it. A claim with no citation cannot be composed, which is the part of the design I would test first if I were reviewing this tool.

The step list is made of a fixed set of line types, and knowing them makes a trace readable at a glance. A successful investigation ran, in order: Run setup · 4 entries, then Tool invoked — inspect_schema via sql.query.read, Result stored — 8 rows, 4 columns, 4 ms with the artifact's id in brackets, Statement drafted with its reasoning, its SQL, a Copy and an Apply to editor, then Tool invoked — run_read_query via sql.query.read, Result stored — 1 row, 2 columns, 3 ms, Report composed — 1 claim, each citing evidence and Run answered. Run setup is collapsed and worth opening: it holds Run opened in agent mode for an investigation with the objective and a Copy, Run started in agent mode, Driven by qwen3-coder:30b — the model's name, on the screen, in the run's own record — and Schema captured — 8 tables, fingerprint ctx_9a0e. Under the whole list sits the sentence that explains why yesterday's trace has no data in it: A run's stored rows are released when the run ends, so a result can be shown only while its run is still going. The trace is kept. The rows are not.

The schema capture is not repeated for every run. When an earlier run has already read the same connection, the line reads Schema reused — 8 tables, fingerprint ctx_9a0e, read by an earlier run and 17 minutes old instead of Schema captured, and Run setup holds three entries rather than four. The fingerprint is computed from the connection's own identity, so a schema that changed under a connection that did not will be reused with its age printed. The age is the part to read.

A plan run hands back something else entirely, and the screen is different enough to describe on its own. Under ANSWER succeeded the answer is the SQL, with its own Copy and Apply to editor, over three chips: Read-only, 8 tables read and the fingerprint ctx_9a0e. Why this statement opens on Checked as a bounded read against the captured inventory. Nothing was executed., and What the statement guard checked is careful about what it is claiming: The run executed nothing. What was checked is what this run read of the schema, which records what exists rather than what your role is permitted to read. The steps are Closing statement, which names the engine and repeats the SQL with a Copy all, and Statement drafted; the run ends on The model finished its plan and stopped. Planning mode has no tools, so it composed no report. The statement and database-time counters are drawn all the same, reading Statements 0 / 42 and Database time 0.0 / 180.0 s for a run that will never spend either.

Operate is different again, and it is the one workflow whose screen has nothing in common with the others. The tool is inspect_operations via db.operations.read rather than sql.query.read, because it sends no statement and calls the provider's curated reporting methods instead. The answer is not one sentence but several claims, each with its own Copy and its own source — Evidence · 5 citations on the run I measured — and the sources come in two shapes: Artifact feffe368 · 1 row via db.operations.read and Schema snapshot ctx_7552 · 3 tables. Every reading carries a caveat the other workflows never print: A moment, not a history: this reading says what the engine reported as it was taken. Refusals show up here as the engine's own words — The database refused the statement over SELECT command denied to user 'studio'@'172.28.0.5' for table 'events_statements_summary_by_digest' — and on a long run you will see the reserve fire as a step of its own, Asked to file its report.

The Evidence section expanded, showing one citation with its artifact ID, a row count, and the exact SELECT that produced the answer.
One citation, with its statement. You can copy the SQL and check the answer yourself, which took me about fifteen seconds.
36

Choosing the workflow yourself

#

Under Advanced sits the choice most people miss. A run can be opened as Automatic, Investigate, Optimize, Assess, Operate or Analyze, and Automatic is what you get if you never look.

The app explains the difference itself: Automatic reads your objective on the server and opens the run for the workflow it names. Naming one yourself skips that reading entirely.

What the app does not explain is what the five are for, and the difference is not cosmetic: each opens with its own budget, its own deliverable and, in one case, its own tools. The plainest description of them is the one the server uses on itself, in the prompt that classifies your objective when you leave the choice to Automatic. Investigate takes a question about the database that is answered from what the run establishes and ends on the statement that answers it. Optimize takes a specific statement that is too slow, and how the engine reaches its rows and hands back a rewritten one, having compared plans it describes rather than executes. Assess takes the state of the data itself: where it is incomplete, inconsistent or surprising and ends on the statement that measures the concern. Operate takes how the database is RUNNING right now: connections, waits, blocking, space and index usage, and is the only one of the five that answers in prose rather than in a statement — which is also why it is the only one that runs on every engine. Analyze takes a question about the data whose answer is a result the user wants to see, and is the only one that offers to run its answer in your editor.

The Advanced section expanded in the agent rail, showing the six workflow choices: Automatic, Investigate, Optimize, Assess, Operate and Analyze.
Five workflows, and Automatic. Automatic is not a sixth: it is the absence of a choice, resolved into one of the five on the server. The sentence underneath is the app's own explanation, not mine.

Automatic reads the objective with the model before the run opens, and says so while it reads: Reading your objective to choose a workflow. When it lands, the line above the run id names what it chose — Opened as Analyze, read from your objective. When it does not, the same line says that instead: Opened as Investigate: your objective could not be classified, so the run investigates rather than being told what it is for. A revenue question can go either way; mine was read as Analyze. Naming the workflow yourself removes both the guess and the round trip that produced it, which is a model call of its own under an eight-second deadline.

Two things about the question box do not follow the workflow. Its label reads What should the run investigate? under all six choices — Optimize, Assess, Operate and Analyze included — over the placeholder Why is checkout slow?; I checked each in turn and the wording never moves. And the box takes 4000 characters. There is no counter and no warning: paste more and it keeps the first 4000 without saying so.

The rail holds one conversation per browser tab, and it tells you when that conversation ends: reload the page, or switch to another connection, and the run you were in is closed with the next question starting a new one. A finished run is appended to a history kept under your own sign-in, and the rail's History surface opens it, twenty conversations to a page and fifty kept in all. Yesterday's question can be opened again rather than only the one in front of you. Reopening reads that run's own record, so the list stays a set of pointers and never becomes the account of what happened.

The History surface is the icon at the top left of the rail. It opens a panel headed History with a Reload run history button, and each row carries the question it was asked, Succeeded or Failed, the word answered where the run answered, then the step count and the time: 11 steps · Sep 20, 2026, 2:05 PM. Fifteen rows were listed on the installation I measured.

The two ways a conversation ends are announced in two different sentences, and only one of them names what it closed. A reload: The conversation this browser was in (1 question, arun_a5cd7611f32447d3a660f0923d901943) ended when the page reloaded. Your next question starts a new one. A change of connection, said after the fact rather than before: Connection changed, so this question started a new conversation.

Who may run the agent is not a role question. Signed in as the lower-privilege user account the rail renders in full — both modes, the badge, the explanation, the question box, Advanced and a live Start — and nothing about it is withheld. What that account sees less of is connections: on the server-side stores a connection a user saves is theirs alone, so the administrator's own connections are absent from its sidebar while the seeded ones are all there. Since the seeded ones are exactly the ones the agent can run on, the effect is that a user account can run the agent on everything it is allowed to run on. The limit is the connection, not the account.

Below 1024 pixels the workspace stops being three panes and becomes four tabs along the bottom of the screen: DB, Schema, SQL and Agent. The rail is the fourth. It opens as a full-height sheet carrying everything it has on a desktop — the two modes, the badge and its box, the question field, Advanced and Start — rather than a reduced version of itself.

A few of the agent's other numbers are fixed in the build, and they are the ones that explain an ending you did not ask for. A model turn gets 90 seconds (AGENT_MODEL_TURN_TIMEOUT_MS, the only one of these you can change); a transport failure is retried twice; a tool call the server cannot read is answered twice before the run stops trying; a conversation carries at most 20 steps of context and 4000 characters of it, with each step's objective trimmed to 200; and a call that would return faster than 250 ms is held to that. History keeps 50 conversations and pages 20 at a time, 100 being the most it will page. The execution profile is versioned and its version is in the run's record — agent-read-only.investigation.1, with operations at revision 2 — alongside agent-handover for the statement that lands in your editor and agent-operations for Operate's reporting calls.

The agent rail's History surface listing two finished conversations with their outcome, step count and timestamp: one succeeded and one failed.
Two finished conversations. The list keeps a failed run as readily as an answered one, which is the point: the run that refused a superuser role is still there to reopen and read.

Settings

The settings that decide how it behaves

Everything else has a sensible default. These are the ones worth reading before you deploy.

Variable Default What it decides
ADMIN_EMAIL admin@libredb.org The administrator account's email.
ADMIN_PASSWORD generated Generated on first run and printed to the log, unless you set it, set AUTH_BOOTSTRAP=off, or run with NEXT_PUBLIC_AUTH_PROVIDER=oidc, where no local password is needed at all.
USER_EMAIL / USER_PASSWORD user@libredb.org / unset A second, lower-privilege account. It exists only if you set the password.
JWT_SECRET generated Session signing, and on the sqlite and postgres stores it also derives the key that seals saved connection secrets. 32 characters minimum; a shorter value stops the server from starting. Under the default local nothing is sealed on the server, because nothing is stored there.
AUTH_COOKIE_SECURE unset Set to false for plain HTTP on a non-localhost host, so the browser keeps the session cookie. Unset, the cookie is Secure in production, with a plain-HTTP loopback request as the one exception.
AUTH_BOOTSTRAP on off turns off credential generation, JWT_SECRET included. Recommended for production, where you supply your own, and you have to supply JWT_SECRET as well, or the server refuses to start and prints what is missing, rather than booting healthy and answering every login with 503.
STORAGE_PROVIDER local local keeps saved connections in the browser. sqlite and postgres keep them on the server, which a team needs. None of the three decides whether the agent can run: that takes a connection the server can rebuild on its own, which means a seeded one. SEED_CONFIG_PATH adds your own; the image already seeds two samples without it.
STORAGE_SQLITE_PATH ./data/libredb-storage.db Where the SQLite store goes. Put it on a volume.
STORAGE_POSTGRES_URL unset Required when STORAGE_PROVIDER=postgres, and the counterpart to the SQLite path above. A normal PostgreSQL URL, with sslmode the part worth getting right: disable for a local database, require for a hosted one.
STORAGE_ENCRYPTION_KEY derived Seals the passwords inside saved connections on the server-side stores. Left unset it is derived from JWT_SECRET, and on sqlite that has a consequence worth knowing before you take a backup: the first run writes that secret to auth-bootstrap.json in the same data directory as the database file, and the chart mounts that directory as one volume — so a snapshot of it carries the ciphertext and the key that opens it, side by side. Setting this from outside that volume closes the gap. A postgres deployment does not have it by default, because the key lives on the app's own filesystem rather than in the database being backed up. Rotating whichever key is in use makes existing stored credentials unreadable: they are dropped from the connection rather than deleted with it, and you re-enter the password once.
NEXT_PUBLIC_AUTH_PROVIDER local oidc switches to single sign-on. OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET and OIDC_ROLE_CLAIM go with it, and OIDC_ADMIN_ROLES is the one that decides which claim values become an administrator. Left unset it defaults to admin.
LLM_PROVIDER unset gemini, openai, ollama or custom. Naming one is not enough on its own: gemini and openai also need LLM_API_KEY, and custom needs LLM_API_URL. With nothing set, the AI features do not render at all.
LLM_MODEL per provider Which model the provider should use. Naming one is optional: each provider has a default, so a key on its own is enough to start. Name it when you want something else — a cheaper Gemini, a local Ollama model you have pulled, whatever your gateway exposes. Agent mode is the case where the choice matters, because it needs a model that calls tools; plan mode does not and is never probed for it.
LIBREDB_AGENT_ENABLED derived The explicit off switch for the agent. Left unset, availability is derived from two things holding at once: an AI provider configured, and a writable directory for the agent's durable ledger, which is WORKFLOW_LOCAL_DATA_DIR — there is no flag that turns it on. The directory is write-probed at startup and the ledger's own version.txt is read from it, so an unwritable path and a ledger written by an incompatible release both come back as a named reason rather than as a missing rail. Set this to false and the agent stays off even with AI configured, which is how you keep the AI features while declining the agent. true is accepted and means the default; it cannot conjure a model that is not there.
SEED_CONFIG_PATH /app/config/seed-connections.yaml A YAML file of pre-configured connections, reloaded about once a minute without a restart; a path ending .json is read as JSON instead, by extension. These are what the agent can run on, because the server can rebuild them from an id alone. You do not have to write one to try the agent: the image seeds two samples on first boot, and the SQLite one, Sample (Employees), qualifies. The LibreDB one does not — agent mode has no read-only statement path on LibreDB, and the rail cannot rebuild that sample either. The file's own schema is at the end of this page.
TRUST_PROXY_HEADERS / TRUSTED_PROXY_HOPS true / 0 These decide what the limiter above counts against, and what the audit log records as the caller's address. With the first at its default the client address is read from X-Forwarded-For, falling back to X-Real-IP; set it to false and every anonymous caller shares one bucket, so one of them tripping the login limiter locks out the rest until the window closes. The second picks which entry in that header is the client. The default of 0 takes the leftmost, which is the one the caller writes — so behind a reverse proxy it should be the number of proxies in front. Too low and a caller chooses the bucket they land in; too high and it keys on a proxy, lumping everyone behind it together. Either way the ip field in the audit log inherits the mistake.
ALLOWED_ORIGINS unset The origins the app will accept a state-changing request from. Unset it works out its own, which is right until a reverse proxy rewrites the Host header to an internal name — a Kubernetes service, a Docker alias — without setting x-forwarded-host. The symptom is unmistakable once you know it: the page loads perfectly and then refuses every action, login included, with a 403 whose body names this variable. Comma-separated; full origins or bare hosts both work.
HOSTNAME per channel Which address the standalone server listens on. The app itself never reads it; the runtime does. Left unset the channel decides, and the channels disagree on purpose: the Docker image and the Helm chart resolve it at startup and prefer ::, which in a Node server takes every address of both families through one socket, falling back to 0.0.0.0 where the namespace has no usable IPv6. The native channels — npx, the .deb and .rpm, Homebrew, Snap — force 127.0.0.1 and treat exposure as something you ask for. Setting it in a container overrules that resolver and pins the container back to IPv4. In a dual-stack Kubernetes cluster that is the one thing not to do: the Service keeps advertising an IPv6 address the pod no longer answers on, and because the kubelet probes only the primary address the pod still reports itself healthy. The chart warns when it sees the combination.
RATE_LIMIT_* 5/300, 20/300, 20/60, 120/60, 5/300 Five buckets, each a count and a window in seconds, and each a pair of variables: RATE_LIMIT_LOGIN_MAX and RATE_LIMIT_LOGIN_WINDOW_SEC for failed logins per client address, five per three hundred seconds; RATE_LIMIT_LOGIN_ACCOUNT_MAX and RATE_LIMIT_LOGIN_ACCOUNT_WINDOW_SEC for failed logins per submitted account, twenty per three hundred; RATE_LIMIT_AI_MAX and RATE_LIMIT_AI_WINDOW_SEC for AI and agent-run requests per signed-in user, twenty per sixty; RATE_LIMIT_QUERY_MAX and RATE_LIMIT_QUERY_WINDOW_SEC for requests that reach a database per user, a hundred and twenty per sixty; and RATE_LIMIT_ANON_MAX and RATE_LIMIT_ANON_WINDOW_SEC for anonymous callers, five per three hundred. Setting a count to 0 disables that bucket; a window under a second is raised to one. The limiter is per process, so a multi-replica deployment has to repeat the budget at the ingress.
BASE_PATH unset The one variable on this page that is not a runtime setting. It builds the app for a mount point such as /tools/libredb so root-relative API calls, editor assets, redirects and cookies keep the prefix. Next fixes it at build time, so the published image cannot be moved with it: wanting a subpath means building your own image with this as a build argument. The chart's config.basePath is a different thing — it prefixes the three default health probes and does not move a prebuilt image.
ADMIN_TOTP_SECRET / USER_TOTP_SECRET unset A base32 secret makes that account present a six-digit code as well as its password; the login answers mfaRequired until it arrives. At least 26 base32 characters, because RFC 4226 wants 128 bits. A value that is not base32, or shorter, does not quietly drop the factor: it stops the login with 503 and names the variable. Local accounts only, and the secret is an environment variable rather than state on disk, because the image runs read-only. ORACLE_CLIENT_LIB_DIR unset Where the Oracle Instant Client libraries are. It is the only way to run the Oracle driver in thick mode, which is what you need for the features the thin driver does not carry. Set it and the driver is initialised from that directory at first use; get the path wrong and the connection fails with a message naming the directory rather than falling back quietly. WORKFLOW_LOCAL_DATA_DIR .workflow-data The directory holding the agent's durable ledger — the record every run is written to and read back from. The default is relative to the working directory, which in a read-only image is not writable, so the image sets it to a path on the data volume. It is write-probed at startup, and its version.txt is read: an unwritable path reports LEDGER_UNAVAILABLE and a ledger from an incompatible release reports LEDGER_INCOMPATIBLE, both naming this variable. Put it on a volume or the agent's history does not survive a restart. VAULT_ADDR / VAULT_TOKEN / VAULT_ROLE unset HashiCorp Vault, for the seed file's ${vault:...} references. VAULT_ADDR is the server; then either VAULT_TOKEN, or VAULT_ROLE for Kubernetes auth, which logs in with the service-account token at VAULT_K8S_TOKEN_PATH (default /var/run/secrets/kubernetes.io/serviceaccount/token). VAULT_NAMESPACE sets the namespace header and VAULT_CACHE_TTL_MS how long a read is cached, sixty seconds by default. Each is missed with a message of its own rather than a generic failure, and the message quotes back the reference it could not resolve: Vault address is not configured: set VAULT_ADDR to resolve, then the reference itself. SEED_CACHE_TTL_MS / NEXT_PUBLIC_MANAGED_POLL_MS 60000 / 1000 The first is the "about once a minute" the seed file is re-read on, and the API hands the figure to the browser so the two agree. The second is how often the page asks the server for seeds it has not seen yet, which is why a connection you add to the YAML appears without a reload. AGENT_MODEL_TURN_TIMEOUT_MS / AGENT_MODEL_TUNING_PATH 90000 / unset How long one model turn may take before the run gives up on it, and a file of per-model tuning for the agent. The tuning file is optional; where none is given /api/agent/config reports its state as unset, which is the quickest way to check whether yours was picked up. LIBREDB_AGENT_THREAD_CONTEXT on Whether a run can see the conversation it is part of. Off, every question starts from nothing but the schema. On, a run carries at most twenty earlier steps and four thousand characters of them. /api/agent/config reports which it is. SQLITE_EMBEDDED_SAMPLE / LIBREDB_EMBEDDED_SAMPLE_PATH on / <data>/sample.libredb The two samples the image seeds on first boot. SQLITE_EMBEDDED_SAMPLE=false switches the SQLite one off entirely; SQLITE_EMBEDDED_SAMPLE_PATH moves it and SQLITE_EMBEDDED_SAMPLE_TEMPLATE names the file it is copied from. LIBREDB_EMBEDDED_SAMPLE_PATH moves the LibreDB one, and LIBREDB_EMBEDDED_SAMPLE=false switches it off. Switching the SQLite one off takes the agent's only ready-made connection with it. WORKFLOW_TARGET_WORLD local Where the agent's runtime keeps its state: local, the ledger directory above, or @workflow/world-postgres. Any other value stops the server with UNSANCTIONED_WORLD_TARGET and prints the two it accepts. LOG_LEVEL info debug, info, warn or error. debug is what names the seed connection a credential failed to resolve for, and the Vault path a secret was read from. CSP_REPORT_ONLY / HSTS_INCLUDE_SUBDOMAINS false / false The first leaves the Content-Security-Policy in report-only mode instead of enforcing it — "false" enforces, "true" stays reporting. The second adds includeSubDomains to the HSTS header, whose max-age is fixed at 15552000 seconds either way. Both take the string rather than a boolean, and an unrecognised value is logged and ignored. OIDC_SCOPE openid profile email The scopes asked for at the authorisation endpoint. Worth setting where your provider puts group or role membership behind a scope of its own, since OIDC_ROLE_CLAIM can only read a claim that was actually issued. LIBREDB_SQLITE_DRIVER / LIBREDB_NO_BANNER auto / unset The first forces the SQLite driver to bun or node rather than letting the runtime choose; a driver that is not available in the environment fails with a message saying which was asked for. The second, set to 1 or true, suppresses the startup banner — the line that prints the version and the address it is listening on. NEXT_PUBLIC_MONACO_VS_PATH /monaco/vs Where the editor's own assets are served from. It is also written into the Content-Security-Policy, so an air-gapped deployment that moves them has to move this as well or the editor loads nothing and says nothing.

Engines with a driver in this build

PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra and Redis. Twenty-six further engines speak one of those wire protocols and connect through an existing driver, which is how sixteen drivers reach forty-two named engines.

The editable grid is not universal. Nine of the sixteen have no in-place row editing: Cassandra, ClickHouse, Couchbase, Druid, Elasticsearch, MongoDB, OpenSearch, Redis and Trino. Eight have no create-table form, the same list without Trino. Druid, Elasticsearch and OpenSearch are the honest read-only cases, with no UPDATE and no CREATE TABLE in the grammar at all; the rest refuse for their own reasons. Where a capability is missing the control is not drawn, so its absence is the only notice you get. Cassandra publishes no row count and no size that is true, so the object browser shows neither rather than showing a number that is wrong.

The seed file, field by field

The file SEED_CONFIG_PATH points at has three keys at the top: version: "1", which is required and is a literal rather than a number; an optional defaults block carrying managed, environment and ssl, each inherited by any connection that does not set its own; and connections, a list of at least one. Every connection needs four things: an id of up to 64 lowercase letters, digits and hyphens, unique across the file; a name; a type, which is one of the seventeen engine keys (postgres, mysql, sqlite, mongodb, redis, oracle, mssql, libredb, couchbase, clickhouse, druid, elasticsearch, opensearch, trino, cassandra, libsql, duckdb); and roles, a non-empty list of "*", admin or user deciding who sees the connection at all. The rest are optional and are the fields the dialog would have asked for: host, port, database, user, password, connectionString, schema, serviceName, instanceName, localDataCenter, authSource, skipObjectScan, plus environment, group, a color as #rrggbb, managed, and an ssl block of mode, caCert, clientCert, clientKey and rejectUnauthorized.

Five of those fields take an indirection instead of a value: password, connectionString, user, host and database. The first form is an environment variable, ${PG_PASSWORD}, which has to be the whole value — upper-case letters, digits and underscores only, and no text around it. The second is Vault: ${vault:<mount>/data/<path>#<key>}, read at resolution time with the VAULT_* settings above. A connection whose environment variable is not defined is skipped rather than failing the file, and the log says which one and which field; a password written in plain text is accepted with a warning naming the connection. Two fields the record has are not in this schema and are dropped if you write them: agentUser and agentPassword. Seed a second connection on a least-privilege user instead.

The file is read about once a minute rather than at startup only, so adding a connection to it is a live operation. A file that does not parse, or that fails its schema, raises an error naming the field and the problem — connections.0.roles: At least one role is required, Connection IDs must be unique. A file that is simply absent is not an error at all: the log records that seed connections are disabled and the app runs without them. managed: true gives everyone the same locked connection with a padlock; managed: false gives each user an editable copy of their own, without one. Both are seeds and both can run the agent — but an unmanaged copy is matched against the seed the server still holds, field by field, so one a user has edited stops qualifying and the rail says it cannot be rebuilt.