A fast, minimal desktop client for DuckDB. Query, browse, and edit your data. Nothing else.
DuckTable speaks to DuckDB Harbor
and requires it. It never links DuckDB and never opens a database file
itself: it reaches every database through a Harbor server, a file on this
Mac or a remote by name, and Harbor owns the engine, the files, and the
versioning. Local files work through Harbor’s spawn-on-demand, the same way
harbor <file> opens them.
One window, three views — and three of the built-in themes. Click any shot for full resolution.
Structure — every column with its type, attributes, and defaults, and the table’s DDL below, drawn as the engine would write it. (Duck Light theme.)
Data — 27k rows paged 5,000 at a time in 28ms, with row numbers, right-aligned numerics, and NULL tags each one keystroke away. (Paper theme.)
Query — a SQL scratchpad above live results. Statements wear bands
in the gutter, line numbers restart per statement to match the engine’s
own LINE n diagnostics, and ⌘Enter sends the statement under the
caret. (Midnight theme.)
With Homebrew:
$ brew install --cask shreeve/tap/ducktable
Or with one command, which drops DuckTable.app into /Applications from
the latest release:
$ curl -fsSL https://raw.githubusercontent.com/shreeve/duckdb-harbor/main/ducktable/scripts/install.sh | bash
Apple Silicon. The app is signed with a Developer ID and notarized by Apple, so it opens on first launch however it was downloaded, the release’s zip from a browser included.
An installed copy is replaced by rename: the old app steps aside, the new one
takes its name, and the old one is removed last, so an install that fails
leaves the DuckTable you had. The installer then registers the bundle with
Launch Services, so Finder, Spotlight and System Settings know it at once.
... | DUCKTABLE_DEST=dir bash installs into another directory.
Uninstall with ... | bash -s -- --uninstall — the app goes; your settings
(~/.config/ducktable) and your databases stay.
After that, DuckTable keeps itself current: DuckTable → Check for Updates…, or say yes to the first-launch prompt and it checks once a day (docs/UPDATES.md).
To build from source: clone the repo and run
ducktable/scripts/macos-app.sh release; scripts/install-local.sh builds
and installs in one step, with the same swap.
Every build is signed with the Developer ID under one identifier,
com.shreeve.ducktable, and the build fails if the app or its executable
reports any other. macOS files an app’s privacy decisions under that name, so
it never changes. Without the certificate, SIGN=- scripts/macos-app.sh
builds ad hoc (docs/UPDATES.md).
Choose File → Open Database URL… when Harbor is already listening on a TCP
port. Give the database a sidebar name, host, and port. localhost
connects directly; any other host makes DuckTable create the SSH tunnel:
Name Production
Host foo.bar.com
Port 9494
DuckTable asks macOS’s /usr/bin/ssh to forward a unix socket of its own to
127.0.0.1:9494 as seen from foo.bar.com, then speaks Harbor through that
socket. The socket sits in Harbor’s runtime directory, which only you can
enter, so no other user of the Mac reaches the database through the tunnel,
as any could through a loopback port. SSH runs unattended and honors
~/.ssh/config, keys, certificates, ProxyJump, ssh-agent, and the macOS
Keychain. Run ssh foo.bar.com once in Terminal if a host key or login still
needs confirmation.
macOS 15 and later gate each app’s traffic to the local network, and the ssh
DuckTable starts counts as DuckTable. For a host on your LAN, macOS decides
per app and, when it asks, shows the reason the bundle gives
(NSLocalNetworkUsageDescription: SSH tunnels to Harbor servers on the hosts
you add). The answer lives under System Settings → Privacy & Security → Local
Network, in one DuckTable row that follows the app across updates because its
signing identifier is fixed. Terminal is exempt from that gate and an app is
not, so when ssh host works in Terminal and DuckTable’s tunnel to the same
LAN host does not, look at that row first.
The SSH process belongs to that database connection. It uses protocol keepalives, stays alive while queries still hold the connection, and is closed and reaped when the database is closed, removed, replaced by another database, or DuckTable exits. Removing the sidebar entry forgets only its connection details; it never changes the database server.
The saved form is intentionally small:
[connection.production]
url = "http://foo.bar.com:9494"
For a local listener, save http://localhost:9494; DuckTable normalizes that
to IPv4 127.0.0.1 when connecting. Use File → Open Database File… when
starting from a DuckDB filename instead of a port.
No charting. No 30-driver matrix. No sync. No plugin ABI. The “lite” is the feature.
Rust and gpui-kit: GPUI (Zed’s
GPU-accelerated UI framework) with its component library, which supplies the
virtualized data table and code editor. Dependencies are pinned to crates.io
releases, except gpui-kit itself, which comes from a pinned commit of
a fork carrying a few
generic changes on top of the release; Harbor’s protocol crates are built from
the sibling harbor/ tree in this repository.
Early releases, moving fast. Working today: the fleet sidebar, the paged
data grid with filters and column control, the Structure view with DDL,
staged cell editing with a Sheets-style keyboard grammar, and the Query
scratchpad with per-statement send. Requires Harbor 0.44.2 or later. See
docs/DESIGN.md for the architecture, the interface and what is planned.
MIT.