DuckPlus is a native Mac app written in Rust and drawn by the GPU, on gpui — the framework Zed is built with. It talks to a DuckDB server over Quack, or opens a .duckdb file straight off your disk. There is no Electron in it, no web view, and no driver list with DuckDB somewhere in the middle.
Nearly done · macOS 12+ · Apple silicon and Intel
| database_nameVARCHAR | schema_nameVARCHAR | table_nameVARCHAR | estimated_sizeBIGINT | |
|---|---|---|---|---|
| 1 | analytics | main | events | 48210332 |
| 2 | warehouse | billing | orders | 2906115 |
| 3 | analytics | main | users | 184907 |
That window is drawn, not screenshotted. Open a table, run one of the server views, double-click a cell in users and save it, then open the query log to see what was sent. The data is invented. The layout, the SQL and both palettes are the app's.
The tree, the editor and the grid are shaped text and rectangles on a Metal layer. The app starts like a Mac app, scrolls like one, and has nothing running in it that was meant for a browser.
It is built for DuckDB alone, so it doesn't reduce anything to the SQL that every database shares. FROM-first queries, DESCRIBE, table functions, DuckDB's types and its admin functions all work as written.
A saved connection writes its name, colour and endpoint to disk. The token goes to the macOS Keychain and nowhere else, so there's no file of credentials sitting in Application Support.
Connecting
The launcher has two tabs and asks for as little as it can. For a server, that's an endpoint and a token. For a file, it's the file. A connection made there is kept with a name and a colour for next time, while one made from the terminal with duckplus connects without saving anything.
-- on the server CALL quack_serve('quack:localhost', token := 'super_secret'); -- on your Mac duckplus quack:localhost -t super_secret
Endpoints are quack:host[:port], and the port defaults to 9494. localhost uses plain HTTP and every other host uses HTTPS, with a Plain HTTP switch for a server on a private network. Test checks the endpoint and the token before you commit to either.
# or double-click it in Finder duckplus ~/data/analytics.duckdb # open it and run something straight away duckplus ~/data/analytics.duckdb -c "FROM duckdb_tables()"
.duckdb and .ddb files open in DuckPlus from Finder. If another process already holds the write lock, it opens the file read-only rather than failing, and a second window on the same file shares the open database rather than locking itself out.
Underneath
DuckPlus embeds an in-memory DuckDB whose only job is to speak Quack. Every statement you run goes to the server wrapped in quack_query, so the server's own parser handles it — DDL and multi-statement scripts included — and the results come back in DuckDB's own vector format instead of passing through a generic driver.
DuckPlus
window
DuckDB
in-memory
DuckDB
your server
-- what ⌘↵ actually sends SELECT * FROM quack_query( 'quack:db.internal:9494', '<your sql>', token := '…' );
Results
Results stay as the Arrow batches DuckDB returned. The grid is virtualized, so it builds only the rows you can see, and it formats only the cells in them. A hundred-thousand-row result costs the same to scroll as a ten-row one, because the rows you aren't looking at never get turned into strings.
The grid stops pulling batches at 10,000 rows by default, and the status bar says “limit reached” when it does. Change it in Settings.
Click a header to cycle ascending, descending and off. If every row is loaded, it sorts them in place. If a table didn't fit, it asks the server again with ORDER BY.
Space opens the whole value in a dialog. Long values and anything that looks like JSON get a button in the cell, and JSON is pretty-printed.
⌘⇧C copies the selected cell's value, or the selected row, column or whole result as CSV with a header line. The clipboard button copies TSV for pasting into a spreadsheet.
Editing
Double-click a cell, or press ↵, and type. Edits are staged in the grid until you press ⌘S, and then they all go in one transaction. Esc first cancels the cell you're typing in, then discards everything staged.
Each UPDATE is paired with a check that its WHERE clause matches exactly one row. If it doesn't, the check raises an error inside the transaction and nothing is saved. A key column that turns out not to be unique would otherwise change several rows, and a row someone else deleted in the meantime would otherwise be skipped without telling you.
BEGIN TRANSACTION; SELECT CASE WHEN count(*) <> 1 THEN error('Expected 1 row where "id" = …') END FROM "analytics"."main"."users" WHERE "id" = '10437'; UPDATE "analytics"."main"."users" SET "plan" = 'team', "mrr" = '49' WHERE "id" = '10437'; COMMIT;
What ⌘S sends for two cells changed in one row. The query log records the whole transaction, so you can see exactly what was sent.
Guardrails
The statement doesn't run. The status bar says what it is and waits. ⌘↵ again runs it and ⌘. drops it. The check looks at words, not substrings, so a column called dropped_at doesn't trigger it, and it can be turned off in Settings once it gets in your way.
⌘. gives you the window back straight away and ignores whatever the query returns later. A timer counts up in the status bar while you wait, so you can tell a slow query from a stuck one.
A SELECT * against a billion-row table fetches the first 10,000 rows and stops. Opening a table from the tree fetches 500, which is a separate setting.
Every saved connection has a colour, and it shows in the title bar of every window on that connection. Red means production, and you'll see it before you run anything.
The server
Under the tree sits a SERVER section: databases, storage, extensions, settings, memory, secrets and running Quack servers. Clicking one doesn't open a special panel. It puts the SQL in the editor and runs it, so the screen you wanted is also the starting point for the query you actually need.
SELECT database_name, type, path, readonly, internal, comment FROM duckdb_databases() ORDER BY internal, database_name;
SELECT * FROM pragma_database_size();
SELECT extension_name, loaded, installed, extension_version, install_mode, description FROM duckdb_extensions() ORDER BY loaded DESC, installed DESC, extension_name;
SELECT name, value, input_type, scope, description FROM duckdb_settings() ORDER BY name;
SELECT tag, memory_usage_bytes, temporary_storage_bytes FROM duckdb_memory() ORDER BY memory_usage_bytes DESC;
SELECT name, type, provider, persistent, storage, scope FROM duckdb_secrets();
FROM quack_server_list();
A local file shows six of them. quack_server_list() needs the quack extension, and a database you opened from disk doesn't load it.
The rest of it
Everything the window sends is logged: editor runs, tables opened from the tree, admin views, re-sorts and saves, each with where it came from, how long it took and what came back. Collapsed, it's one line showing the latest query. It keeps the last 500.
⌘L formats the selection, or the whole buffer, and ⌘Z undoes it. Keyword case is left alone, because uppercasing keywords also uppercases identifiers like events or date. If the result differs from your SQL by anything other than whitespace, it's thrown away and nothing changes.
On a server with several attached databases, the dropdown in the title bar picks one. Queries then run with USE and the tree shows only that database. A saved connection can remember which one it opens into.
Databases, then schemas, then tables and views, with estimated row counts. ⌘P filters it. Clicking a table opens it in the grid without touching the editor, and there's a Structure button for DESCRIBE.
JetBrains Mono is built into the app and used for the editor, the grid and table names, so a screenshot from one machine looks the same as one from another. Fonts, theme and row striping are all in Settings, and changes apply to every open window immediately.
DuckPlus Night and DuckPlus Day follow macOS, or can be fixed in Settings. The yellow is reserved for keywords, the caret, the selection and the one button you're meant to press, which is why it still stands out.
Connections
Saved connections go in folders you can drag between, sorted by when you last used them. The metadata goes in connections.json, which is safe to read because the token isn't in it. The token goes to the Keychain under its own entry, and deleting the connection deletes the token too.
Paste a Quack endpoint and token. Press ↵ to connect.Tokens are kept in your system keychain.
Keys
Nearly every command is also in the menu bar, so if you forget a shortcut, the menu shows it.
Fine print
It's better to know these before you install it.
DuckDB's own client/server protocol is marked beta until DuckDB 2.0. DuckPlus goes through quack_query rather than ATTACH 'quack:…', because attaching still has catalog gaps.
Quack has no remote cancel yet. ⌘. frees the window at once and throws the result away, but the server finishes the statement it was given.
No Postgres, no MySQL, no SQLite tab. That is the point of it, but it is worth saying before you go looking for a driver list.
The embedded engine downloads the quack extension into ~/.duckdb/extensions the first time. Local files do not need it.
Rows are updated by their primary key, a UNIQUE constraint, or a column you pick. DuckLake tables declare none, so you pick one — and every save checks it matched exactly one row.
The keychain code already speaks Windows and Linux, and gpui runs on both. The installer, the bundle and the Finder integration are macOS today.
DuckPlus is in its last round of polish before the first release. When it ships, you'll get a Mac app and a duckplus command for your terminal, and all it needs is a DuckDB server or a file to open.