coming soon overdb is being prepared for open source. The query editor, history, plan analysis, slow queries, fan-out, and schema drift are working today. There are no public builds yet. where it stands

A desktop database client for Postgres, MySQL, MariaDB, SQLite, DynamoDB, and the managed servers built on them

Every query, every plan, every environment.

Browse tables, filter results, and edit a value in place. Write queries with completion from your real schema or describe one and let your own AI write it, search every statement you've run, and see plans that show where the estimate went wrong. Because the same schema lives in local, staging, and prod, overdb can also run a query on all of them and diff the results. Read-only until you say otherwise, with safeguards matched to each engine.

over·db  — a client that sits over your databases. It doesn't change your schema, and it doesn't send your rows anywhere.

overdb orders-db · 3 environments
orders-db local, staging, prod read-only auto-commit Run on 3  ⌘↵
1select status, count(*) as orders2from orders3where created_at > now() - interval '1 day'4group by status;
local3 rows4 ms
stage3 rows11 ms
prod4 rows38 ms · baseline
statuslocalstagingprod
paid1,2041,1981,212
shipped866861871
refunded3441
pending_reviewabsentabsent7
3 of 3 succeeded 2 rows differ from prod nothing written

One query, three servers, one diff against the environment you trust.

How you'd use it

From a slow page to a fix you trust, in five steps.

A page got slow overnight. This is the path through overdb, and each step links to the part of this page that shows it.

  1. 1

    Connect once

    Add local, staging, and prod, tag each with its environment, and group them into an environment set with prod as the baseline. Every connection opens read-only.

    New connection tag prod add to set

    How writes stay off
  2. 2

    Paste the slow query

    Copy the statement straight from your ORM log and fill its placeholders in the bar under the editor. No log handy? Describe the query and your AI writes it into the editor.

    Paste fill ? Run

    Building a query with AI
  3. 3

    Explain it and read the river

    Run Explain. The river shows where the server reads rows and throws them away, and where a loop multiplies them. The thin places and the orange cones are where the time goes.

    Explain plan view

    How to read a river
  4. 4

    Run it everywhere

    Run the same statement on the whole set. If prod scans a table where staging uses an index, the plans say so side by side, and a server that times out doesn't stop the others.

    Run on set compare plans

    Fan-out across environments
  5. 5

    Ship the fix safely

    Compare schemas to confirm the index exists on staging only, then copy the proposed DDL. On a prod connection, enabling writes means typing its name first.

    Schema drift copy DDL

    Ranked schema drift

The basics

Open a table. Filter it. Fix a value. The everyday work, done carefully.

Most of the time you just want to see what's in a table, narrow it down, and change a value. Overdb handles all of that, and each step is careful not to give you a wrong answer or change the wrong row.

select * from orders limit 200;
status = paid refunded_at is null created_at ↓ filtered and sorted by the server
id customer_id status total note refunded_at line_count computed
19041248213paid42.50''NULL2
29039848213paid129.00gift wrapNULL5
39037117720paid18.00NULLNULL1
49035530581paid260.75split shipmentNULL7
59034048213paid9.99''NULL1
200 rows 14 ms writes on for local ⌘C copies as TSV · right-click for CSV, JSON, INSERT, Markdown

Update one row in orders?

update orders set total = $1 where id = $2
-- $1 = '129.00'   $2 = 90398

Addressed by its primary key, so it changes exactly this row.

Cancel Update
A first look, one click
Clicking a table asks for 200 rows. On DynamoDB it asks for 5 items, so a quick look never turns into a paid full-table scan. Results default to 1,000 rows, not 100,000.
Filters that cover the whole table
Filtering or sorting a column runs the query again on the server, with your statement wrapped rather than rewritten. The answer covers every row, not just the ones already loaded.
Edit a value in place
A cell is editable only when its row can be found by a primary key or unique index, so an update changes exactly one row. The value is bound, never pasted into SQL, and you see the statement before it runs.
NULL is not empty
NULL and an empty string look different in the grid, and every copy format says what it does with NULL. Only JSON keeps it exactly.
Errors with the fix attached
Mistype a column and overdb matches it against the schema, so id becomes er_id in one click. Anything more than a typo can go to your AI for a repair, which lands in the editor.

Ask your AI

Describe the query you want. Read it before it runs.

Type what you're after in plain words, or ask a question about the schema. Overdb sends it to the claude, codex, or gemini CLI you already use, along with the table names, column types, and constraints it needs. The SQL comes back into your editor and doesn't run until you press Run.

Ask orders-db · 3 questions claude ▾

Yesterday

Which tables reference customers?

orders, refunds, and customer_addresses, each through a customer_id foreign key.

Today

EU customers who haven't ordered in 90 days, with their last order date

This uses a left join so customers with no orders at all are kept, then keeps anyone whose latest order is older than 90 days or missing.

select c.id, c.email, max(o.created_at) as last_order_at
from customers c
left join orders o on o.customer_id = c.id
where c.region = 'eu'
group by c.id, c.email
having max(o.created_at) < now() - interval '90 days'
    or max(o.created_at) is null;
Insert New tab Explain
saw 4 of 41 tables
Ask about this database, or describe a query… ⌘↵
Nothing runs automatically.Send

orders-db prod read-only

Run  ⌘↵
-- Suggested by claude
select c.id, c.email, max(o.created_at) as last_order_at
from customers c
left join orders o on o.customer_id = c.id
where c.region = 'eu'
group by c.id, c.email
having max(o.created_at) < now() - interval '90 days'
    or max(o.created_at) is null;

Added to the end of this tab. Not run yet.

What the AI sees

  • Table and column names
  • Column types and constraints
  • Plan statistics, when you ask it to explain

What it never sees

  • Your rows or any values in them
  • Passwords or connection secrets
  • Anything on a connection you didn't ask about
Your AI, your login
Overdb finds whichever of claude, codex, or gemini you have installed and uses its existing login. There's no API key to enter and no extra subscription.
Nothing runs by itself
Insert adds the SQL to the end of your tab with a note saying which AI suggested it. You can also open it in a new tab, or Explain it to see the plan without running it.
See what it looked at
Every answer says which tables it saw, like "saw 4 of 41 tables". If an answer is wrong, click that line to choose the tables it should always see.
Threads you can come back to
Each connection keeps its own conversation, saved between sessions and searchable by any word in a question, answer, or statement.

Everyday querying

An editor that knows your schema. A history you can search.

Most of the day is one connection and a query to write. Completion comes from the real catalog, so a join suggestion fills in the ON clause from the foreign key. You can paste a statement straight from an ORM log and fill in its placeholders. Every statement you run is kept, grouped, and searchable.

orders-db local

Run  ⌘↵
select o.id, o.total, c.email
from orders o
join cu
where c.client_name = ? and o.status = :status
Parameters ? client_name 'hp' this environment :status 'paid' default

Values are bound, never pasted into the SQL, and remembered per environment and per connection.

completion from the catalog · placeholders bound, not pasted · history kept across restarts

The plan river

Rows are the water. Slow queries are where it spills.

A plan table hides the two things that make a query slow. Thrown-away rows are a filtered percentage six columns from the count it applies to. Nested loops are a loops number you multiply by hand. Overdb draws the main line of the plan as a stream as wide as the rows moving through it, so both show up at a glance.

The work this query does explain analyze

select … from customers c join orders o on o.customer_id = c.id where c.region = 'eu' order by o.created_at desc limit 20

Reads 48,120 rows to return 20. Most of that is customers, which has no index on region.

Reads 1,200 rows to reach the join. Customers is fixed. The next cost is the sort: 37,200 rows ordered so 20 can be kept.

A stream flows from select through customers, orders, and sort to the result. Before the fix, it is 48,120 rows wide into customers and narrows sharply as 46,920 rows fail the WHERE clause. It flares as a nested loop reads 31 orders for each of the 1,200 remaining customers, then narrows again as the sort keeps 20 of 37,200 rows. 48,120 1,200 × 31 each −46,920 fail the WHERE, 2.5% survive 1,200 1,200 × 31 each 37,200 20 −37,180 rows go no further select 48,120 rows 1,200 rows result 20 · 2,140 ms 20 · 412 ms customers c · full scan reads 48,120 no index on region customers c · via customers_region_idx reads 1,200 orders o · via orders_customer_idx 31 × 1,200 runs = 37,200 sort created_at desc sorts 37,200
Width is rows
The stream is as wide as the rows moving between two steps. It uses a square-root scale so 20 rows next to 48,120 still shows up. The numbers on the labels are exact.
A narrowing is waste
The orange cone, with rows falling out of it, is rows the server read and threw away. The label under it puts the drop next to the filtered share it came from.
A flare is a loop
× 31 each means a nested loop reads 31 rows for every row that arrives. The small marker runs backward because the loop repeats. The node does the multiplication for you.
A glow is a suspect
A step glows when it reads far more rows than it passes on, or reads a table without an index. That's usually where to look first.
Estimated, not measured
With plain EXPLAIN, every count is the planner's guess and gets a ≈. With ANALYZE, the counts are what actually happened.

width is rows · narrowing is waste · flare is a loop

Performance

Not a prettier plan tree. The row it went wrong on.

Under the river is the ledger: every step with its estimated and actual rows side by side, because a big gap between them is usually the cause. Alongside it: what the server itself spends time on across every client, from pg_stat_statements or performance_schema, measured from when you opened the pane.

Ledger the table under the river · 2.14 s

nodeestimatedactualoff bytime
Limit20202.14 s
Sort · created_at desc1,20041,87035×2.13 s
Seq Scan on orders1,2003,712,0043,093×1.98 s

Explained by your AI claude, codex, or gemini, whichever you use

The planner expected about 1,200 rows for customer_id = $1 and read all 3.7M, because there's no index on orders.customer_id. An index on (customer_id, created_at desc) would also remove the sort.

Slow queries since you opened this · 4 min

  • select … from orders where customer_id = $11,204 calls · 1.9 s mean
  • update sessions set seen_at = $1 …88k calls · 4 ms mean
  • select count(*) from events …12 calls · 610 ms mean
99.2%cache hit
38 / 200connections
3unused indexes

estimate versus actual · what the server says it spends time on · health without zeros for missing data

Environments

Run it once. Read every answer.

An environment set is the same logical database across environments, pinned to a baseline: orders-db on local, staging, and two prod regions, where prod-us is the truth. A query fans out to every member, and each one reports its own result. One server timing out doesn't cancel the other three.

orders-db environment set

Baseline prod-us. Same statement, four servers, four outcomes.

$1 customer_id = 48213
select id, status, total from orders where customer_id = $1 order by created_at desc limit 20;
  • local 20 rows Index Scan · orders_customer_idx 3 ms
  • staging 20 rows Index Scan · orders_customer_idx 9 ms
  • prod-us 20 rows Seq Scan on orders · 3.7M rows read 2.14 s
  • prod-eu timed out canceling statement due to statement timeout 30.0 s

The plans diverge

prod-us seq-scans orders, estimating 1,200 rows and reading 3.7 million, where staging uses orders_customer_idx. That index exists on staging and not on prod-us.

-- proposed. copy it and run it yourself.
create index concurrently orders_customer_idx
  on orders (customer_id, created_at desc);

one statement · every environment · no abort on first failure

Schema drift

Drift, ranked by how much it matters.

Compare every member of a set against its baseline, catalog to catalog, without running a statement. A missing column and a differently spelled varchar aren't the same news, so overdb doesn't report them at the same volume. Findings come back as breaking, notable, or quiet, so the typos don't bury the missing column.

staging compared with prod-us baseline

Read from both catalogs. No statement was run.

3 breaking 2 notable 9 quiet

Breaking

  • orders.refund_reasonon prod-us, missing on staging
  • payments.amountnumeric(12,2) on prod-us, integer on staging
  • orders_status_checkallows pending_review on prod-us only

Notable

  • orders_customer_idxon staging, missing on prod-us
  • customers.localedefaults to 'en-US' on prod-us, 'en' on staging

Quiet

  • 9 columnsvarchar(255) and character varying(255): the same type, spelled two ways

To bring staging in line with prod-us returned as text · there is no Run button

alter table orders add column refund_reason text;
alter table payments alter column amount type numeric(12,2);
-- orders_status_check: review by hand, dropping a constraint is never generated

catalog to catalog · ranked, not listed · nothing destructive is runnable

Overlay, not ownership

Read-only until you type the name.

Enabling writes is a setting on each connection, and it stays set, because "this is my local scratch database" doesn't change from day to day. On a connection tagged prod, turning it on asks you to type that connection's name. The mistake worth preventing is running the right statement on the wrong server.

Read-only
engine-level enforcement for SQL databases; fail-closed classification plus read-only IAM for DynamoDB
Transactions
manual mode keeps one open so a DELETE can be reviewed, and rolls back after 90 s idle
AI prompts
schema context, SQL, errors, and plan statistics; result rows are not intentionally added
Credentials
OS keychain, environment variable, or a 1Password op:// reference

Enable writes on orders-db?

This connection is tagged prod. Writes stay enabled until you turn them off. Type orders-db (prod-us) to continue.

orders-db (prod-u
Cancel Enable writes

Cancelling someone else's session in Live health asks for the same thing.

Everything else in the window

Ten databases, done properly.

The grid, the schema browser, charts, and health checks you'd expect, for ten kinds of database instead of every one ever made. Postgres-compatible and MySQL-compatible servers get the differences that matter to them rather than being treated as plain Postgres or MySQL.

Postgres family
PostgreSQL · Amazon Redshift · Aurora PostgreSQL · CockroachDB · TimescaleDB
MySQL family
MySQL · MariaDB · Aurora MySQL
And
SQLite · Amazon DynamoDB

Overdb works out which one it's talking to when it connects, so Redshift doesn't show up as plain Postgres. Picking a managed type fills in its usual port and turns on TLS where the server requires it: 5439 for Redshift, 26257 for CockroachDB.

01

Connection groups

A durable grouping: "these are Payments." Groups are the sidebar's sections and the target for bulk actions. A connection can be in a group and an environment set at the same time. The two ideas don't overlap.

02

Schemas and databases

Switch schema or database from the query header. On MySQL that's a database, on Postgres a schema in the connected database, and the catalog is read again when you switch, so completion stays accurate.

03

Performance you can act on

Estimated rows next to actual rows, because that gap is the useful signal. Plans are compared across environments, and Slow queries reads pg_stat_statements or performance_schema from the time you opened the pane, so you can watch a deploy land.

04

ORM placeholders

Paste a statement straight from an ORM log, with ?, :clientName, or #{clientName}, and fill the values in a bar under the editor. Values are bound, never pasted into the SQL, and remembered per environment and per connection.

05

ER diagram

The foreign-key graph, read from the constraints the server holds and never guessed from column names. It opens on one table and its neighbors, because a 900-table diagram tells you nothing. Export to SVG or PNG.

06

Live health

Sessions and what they're waiting on, connection headroom, cache hit ratio, table sizes, and indexes the planner has never used. When a server won't report a number, overdb says so instead of showing zero.

07

Charts from any result

Line, bar, area, or scatter. Axes and series are picked from the column types, so there's nothing to configure. A null is drawn as a gap, never a zero, and a chart of a truncated result says it's truncated.

08

A grid that stays fast

Rows stream in chunks from a separate process for each connection, and the grid only draws what's on screen. Sorting by a column asks the server again. It doesn't reorder the 1,000 rows you happen to have loaded.

09

History and saved queries

Every statement you run is kept across restarts and grouped with a run count. Search by text, connection, or outcome. Give a statement a name and it becomes a saved query.

Why overdb exists

Four tabs, one query, and a lot of squinting.

Once overgit was handling the repos, the next thing we kept doing by hand was one layer down. Run a query on local. Run it again on staging. Paste it into a prod tab, compare the three results by eye, and hope you're actually connected to the server you think you are.

Overdb makes that comparison the main feature instead of something you do in tabs. It follows the same rules as its siblings: it's an overlay, it's local, and you can check what it does. It doesn't touch your schema, it doesn't send your rows anywhere, and when it isn't sure what a server meant, it tells you.

Sibling of overcli and overgit. Written & maintained by Lionel Farr and Owen Farr.

Before you point it at prod

Questions your DBA will ask.

Most of the answers come down to one thing: overdb reads by default and asks before it writes.

When can I use it?

Not quite yet. Overdb is pre-v0.1. The engine layer, fan-out, schema drift, the Ask panel, and health all work, but it isn't packaged for release. When it is, it'll be listed here and on the Codelions GitHub.

What will it cost?

Nothing. Like overcli and overgit, it's Apache 2.0, with no account, no server, and no paid tier. The AI features use a CLI you already have and are already logged in to.

Can it write to prod by accident?

Every connection starts read-only. Postgres uses BEGIN READ ONLY, MySQL uses START TRANSACTION READ ONLY, and SQLite opens with its readOnly flag. DynamoDB has no server-side read-only mode, so overdb fails closed on statements it cannot classify; use a read-only IAM policy as the durable boundary. Turning writes on for a prod connection means typing that connection's name.

Does the AI see our data?

Overdb does not intentionally add result rows or bound parameter values to prompts. Depending on the feature, a prompt can include your question, schema metadata, SQL text, server errors, and plan statistics, so literals or database errors may still contain sensitive values. The prompt goes to the claude, codex, or gemini CLI already on your machine, and proposed SQL goes into the editor without running.

Which engines are supported?

Ten: PostgreSQL, Amazon Redshift, Aurora PostgreSQL, CockroachDB, and TimescaleDB; MySQL, MariaDB, and Aurora MySQL; SQLite; and Amazon DynamoDB. They run on four drivers, and overdb detects which server it's connected to so it can handle the places they differ, like Redshift having no indexes or MariaDB's performance views. SQL Server, Oracle, and document stores like MongoDB aren't supported.

Where are passwords stored?

In the operating system's encrypted storage, in a separate file from the rest of the app's state. You can also use an environment variable, or a 1Password op:// reference that's resolved when you connect, so the secret is never saved in overdb at all.

Coming soon

No download yet.

Overdb is being prepared for open source alongside overcli and overgit. The first public build will be announced here with a download table, like its siblings have.

Where overdb stands
Status
pre-v0.1, in active development
Postgres family
PostgreSQL, Redshift, Aurora PostgreSQL, CockroachDB, TimescaleDB
MySQL family
MySQL, MariaDB, Aurora MySQL
Also
SQLite, Amazon DynamoDB
Platforms
macOS, Windows, Linux
License
Apache 2.0
Stack
Electron, React, TypeScript, one process per connection