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.
A desktop database client for Postgres, MySQL, MariaDB, SQLite, DynamoDB, and the managed servers built on them
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.
1select status, count(*) as orders2from orders3where created_at > now() - interval '1 day'4group by status;
| status | local | staging | prod |
|---|---|---|---|
| paid | 1,204 | 1,198 | 1,212 |
| shipped | 866 | 861 | 871 |
| refunded | 3 | 4 | 41 |
| pending_review | absent | absent | 7 |
One query, three servers, one diff against the environment you trust.
How you'd use it
A page got slow overnight. This is the path through overdb, and each step links to the part of this page that shows it.
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
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
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 riverRun 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 environmentsCompare 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 driftThe basics
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;
| ⚿ id | customer_id | status | total | note | refunded_at | line_count computed | |
|---|---|---|---|---|---|---|---|
| 1 | 90412 | 48213 | paid | 42.50 | '' | NULL | 2 |
| 2 | 90398 | 48213 | paid | 129.00 | gift wrap | NULL | 5 |
| 3 | 90371 | 17720 | paid | 18.00 | NULL | NULL | 1 |
| 4 | 90355 | 30581 | paid | 260.75 | split shipment | NULL | 7 |
| 5 | 90340 | 48213 | paid | 9.99 | '' | NULL | 1 |
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.
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
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.
Yesterday
orders, refunds, and customer_addresses, each through a customer_id foreign key.
Today
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;
-- 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
What it never sees
claude, codex, or gemini you have installed and uses its existing login. There's no API key to enter and no extra subscription.Everyday querying
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.
select o.id, o.total, c.email
from orders o
join cu
where c.client_name = ? and o.status = :status
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
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.
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.
filtered share it came from.× 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.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
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
| node | estimated | actual | off by | time |
|---|---|---|---|---|
| Limit | 20 | 20 | 1× | 2.14 s |
| Sort · created_at desc | 1,200 | 41,870 | 35× | 2.13 s |
| Seq Scan on orders | 1,200 | 3,712,004 | 3,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 meanupdate sessions set seen_at = $1 …88k calls · 4 ms meanselect count(*) from events …12 calls · 610 ms meanestimate versus actual · what the server says it spends time on · health without zeros for missing data
Environments
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.
Baseline prod-us. Same statement, four servers, four outcomes.
select id, status, total from orders where customer_id = $1 order by created_at desc limit 20;
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
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.
Read from both catalogs. No statement was run.
Breaking
orders.refund_reasonon prod-us, missing on stagingpayments.amountnumeric(12,2) on prod-us, integer on stagingorders_status_checkallows pending_review on prod-us onlyNotable
orders_customer_idxon staging, missing on prod-uscustomers.localedefaults to 'en-US' on prod-us, 'en' on stagingQuiet
9 columnsvarchar(255) and character varying(255): the same type, spelled two waysTo 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
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.
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.
Cancelling someone else's session in Live health asks for the same thing.
Everything else in the window
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
Most of the answers come down to one thing: overdb reads by default and asks before it writes.
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.
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.
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.
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.
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.
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
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.