Features
The full tour of what Lizard does today. Every section here describes shipped behaviour — see the roadmapfor what isn't built yet.
Connections
- Postgres, MySQL, and MongoDB side by side in one console, mixed freely across services.
- Two roles per connection — a read role and a write role, with separate credentials. Analytics, AI, and charts only ever get the read one.
- Credentials encrypted at rest (AES-256-GCM) in Lizard's own SQLite file. Nothing is written to your databases.
- Searchable, paged pickers — every place you choose a connection is a combobox that searches server-side, because a fleet has more connections than a dropdown can hold.
Browsing and editing
The whole CRUD UI is derived from schema introspection — no per-table configuration, no generated admin scaffolding to maintain.
- Browse, filter, sort, and edit every table across every schema and database.
- Reference columns render as labels, not ids — a foreign key (real or virtual) becomes a searchable picker showing the display column you nominate, falling back to the raw value when a row has no label.
- Virtual relationships let you declare a foreign key that doesn't exist in the database — including across connections and engines. No DDL is ever run against your databases.
- Per-table "Customize" settings: display column, hidden columns, labels, and virtual relationships.
- Export to CSV or JSON, honoring the current filters, sort, search, and which columns you have visible. Pick a row limit, and optionally export any foreign key as a resolved "(label)" column alongside the raw value. A result under the row-limit threshold streams straight back as a download; a larger one becomes a background job with a cancel button, tracked from anywhere in the app until you come back to download it from Settings → Exports.
Filters
Filters are built from the column's real type, so a text column and a timestamp column get different controls and different semantics.
Dates and times mean the whole day
A timestamp filter has a date picker and a separate, optionaltime picker. Most of the time you only want the date — so leaving the time blank means what you'd expect it to mean:
| You pick | Lizard runs |
|---|---|
= 2026-01-01 (no time) | the entire day — >= 2026-01-01 00:00:00 and < 2026-01-02 00:00:00 |
> 2026-01-01 (no time) | after the end of that day |
< 2026-01-01 (no time) | before the start of that day |
between two dates | start of the first day → end of the last day |
| either side with a time | that exact instant; the other side still expands to its day boundary |
The expansion is deliberately a half-open range rather than a cast like DATE(created_at) = '2026-01-01': wrapping the column in a function makes the predicate unsargable, so any index on that column stops being usable. A range comparison keeps the index in play, which is the difference between an index seek and a sequential scan on a large table.
The same rules drive the dashboard date-range picker: from is the start of its day, tois the end of its day, times are optional on both ends, and a time you didn't pick never shows up in the label or the URL.
Timezones
A date range is meaningless without a timezone: "yesterday" in Berlin and "yesterday" in Chicago select different rows. Lizard resolves this once, at the session level, instead of converting timestamps ad hoc in a dozen places.
- The browser's IANA timezone is captured and sent with each request.
- The server validates it against the platform's own timezone database and falls back to UTC if it isn't a zone it recognises — the value ends up in a database session setting, so it is treated as untrusted input.
- Each pooled connection has its session timezone set to match, lazily and only when it changes:
SET TimeZoneon Postgres,SET time_zoneon MySQL, and the equivalent on the embedded DuckDB used for federated queries. - Timezone is part of the query cache key, so two people in different zones never see each other's results.
The upshot: day boundaries, date_trunc buckets in charts, and rendered timestamptz values all agree with each other and with your wall clock, without any timestamp arithmetic in application code.
AI Explore
Ask a question in plain English; an agent works out the query. Claude is the default, with OpenAI and Gemini also supported. See Architecture for the tool loop itself — what follows is what makes it fast enough to sit in front of.
- Bring your own OpenAI-compatible endpoint. On the OpenAI provider,
OPENAI_BASE_URLpoints the model client at a LiteLLM proxy, Azure OpenAI, or any self-hosted gateway instead of api.openai.com — for keeping model calls on infrastructure you control. - Never a schema dump. The model starts from a name-only skeleton — connections, schemas, table names, no columns — and pulls detail on demand with
search_schema,describe_table, andget_relations. - The skeleton is tiered and budgeted. It's generated lazily and stops the moment it hits its size budget, marking itself truncated rather than blowing the context window on a fleet that doesn't fit. Search still reaches everything that was cut.
- Prompt caching. The stable prefix — instructions and skeleton — is marked for caching, so follow-up turns in the same session skip re-reading it.
- The catalog is served stale while it refreshes. Past its TTL you get the last known schema immediately and the re-introspection happens behind you, so only the very first question about a connection ever waits on introspection.
- Live streaming and a real Stop. Every step streams as it happens, and stopping aborts the in-flight model call and cancels the database work it started.
- The SQL is always shown, always editable, and always runs through the same Guard and read-only role as anything you typed yourself.
Dashboards
A query worth keeping is worth reading. Saving a result produces a dashboard — a page of blocks, where a block is one query plus how to display it. Saved queries as a separate concept are gone; every saved result is a dashboard now, down to a single block.
- Four views over the same query — table, chart, kanban, and gallery. Switching view never means rewriting the query. Kanban and gallery let you pick which column is the row's identity and which is its title, since an arbitrary
SELECThas no primary key to assume one from. - Each block can target a different database, or a federated query spanning several.
- Variables — text, single- or multi-select, and date range. Multi-select has an optional All that means "no filter", not "every option expanded into an IN list". Values live in the URL, so a filtered dashboard is a link you can send someone.
- Optional SQL blocks — wrap a clause in
[[ ... ]]and it's dropped from the query whenever the variable inside it is empty, instead of every dashboard hand-rolling its own conditionalWHERE-clause string-building. - An optional date range, enabled per dashboard rather than forced on every one, wired to the same whole-day semantics as table filters.
- Tags and server-side search in the dashboard library, so it stays navigable past a few dozen.
- Grid or solo layout — a resizable grid of panels, or one block filling the whole page, which is what a saved query or a kanban board actually is. Adding a second panel promotes a solo page to a grid automatically.
- Duplicate as save-as, and a full-screen panel editor — describe the panel in English or write the SQL directly — for when a dialog isn't enough room.
- Import/export as JSON — a dashboard's full definition (panels, variables, layout) is one portable document you can version, diff, or hand to someone else's Lizard, and the library can create a new dashboard straight from an imported file.
- Panel edits are a draft until you hit Save. Adding, editing, moving, or deleting a panel only touches local state — nothing is written until Save commits the whole batch in one go and snapshots a restorable version (capped at 100 per dashboard). Leaving with unsaved changes asks first.
Built for a fleet
The design assumption is a database per microservice, thousands of them. That rules out anything that walks the fleet, so every list, cache, fan-out, and pool has a ceiling.
| Bound | Why it exists |
|---|---|
| Introspection requires an explicit connection scope | There is no "whole fleet catalog" call to accidentally make — asking for one with an empty scope is an error, not a full sweep. |
Global search is scoped by connection/table | Search splits the scope you typed and fans out only to that, instead of querying every database. |
| 1,000-row cap on every result | Applies to hand-written, AI-generated, and federated queries alike, with the truncation surfaced in the UI rather than hidden. |
| 30s read statement timeout, 45s federated | Enforced by the database itself, not just the client, so a runaway query dies server-side. |
| 8 connections introspected at once, 20s each | An unbounded fan-out across a thousand hosts is a thundering herd; a slow database is reported as one failed entry while the rest proceed. |
| Catalog cache bounded by column count (~400k) | Counting columns rather than connections holds up across very different fleet shapes — a hundred huge schemas and ten thousand tiny ones settle at a similar footprint. |
| Connection pools expire after 10 idle minutes | Browsing five hundred databases used to leave five hundred live pools behind forever. Idle ones are now closed and forgotten, swept on access. |
| Connection lists page at 20 (100 max) | An endpoint that fills a dropdown has no business returning thousands of rows; the pickers search instead of scrolling. |
Access control and audit
- Admin, editor, and viewer roles, plus per-connection read/write grants.
- Teams — grant a connection to a team once instead of person by person; members inherit whatever the team can read or write. A team has its own managers, who can staff its roster and adjust its grants without needing admin rights.
- Dashboard sharing is one exclusive choice — private, specific teams, or everyone signed in — instead of two settings that could disagree with each other.
- An audit log of every query and every write, with tiered, configurable retention.
- Every statement — CRUD, AI, chart, or dashboard — passes the same SQL Guard: one parsed
SELECT, an enforced limit, a timeout, and a read-only transaction. See the security model for the details.