planche
v0.8.11
Published
Local agent for Planche — lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle/Neon/Supabase/Cloudflare D1 databases and local Durable Object storage from your machine.
Maintainers
Readme
planche
Local agent for Planche, a browser-based SQL client for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, Neon, Supabase and Cloudflare D1.
Browsers cannot open TCP connections to a database, so Planche runs this small
agent on your machine. The app in your browser talks to the agent over
127.0.0.1, and the agent talks to your databases. Credentials and query
results stay on your computer — nothing is sent to planche.dev.
Quick start
With Node.js 22 or newer:
npx plancheOr install it — no Node.js required, and it starts instantly afterwards:
curl -fsSL https://planche.dev/install.sh | sh # macOS, Linux
irm https://planche.dev/install.ps1 | iex # WindowsThe installer puts everything under your home directory (never asks for root),
uses the Node.js already on the machine when it is 22 or newer and downloads a
private one otherwise, and leaves a planche command on your PATH. It then
starts the agent for you, since a shell only picks up a new PATH in a new
terminal (PLANCHE_NO_START=1 installs without starting). Update it with
planche upgrade, remove it with planche uninstall — your settings in
~/.planche are kept either way.
The agent starts on port 8888, prints a pairing link and opens it in your default browser:
https://planche.dev/#agent=http://127.0.0.1:8888&token=…Opening that link pairs the app with the agent. From then on you can add connections in the app as usual; every connection is opened by the agent on your machine.
The token is random on every start. If you restart the agent, the app shows "agent restarted" and asks you to open the new link (or set a fixed token, see below).
Usage
npx planche [options]
-p, --port <n> Port to listen on (default: 8888)
--addr <host> Address to bind (default: 127.0.0.1)
--token <str> Auth token (default: random on each start)
--print-token Print the token (hidden by default; the pairing link is enough)
--no-open Print the pairing link without opening a browser
--verbose Log every request
planche upgrade [version] Update an installed copy (not needed with npx)
planche uninstall [--yes] Remove it again, keeping ~/.planche
-h, --help Show helpEvery option can also be set through an environment variable:
| Variable | Option | Notes |
| ------------------------- | ---------- | --------------------------------------------------- |
| PLANCHE_PORT | --port | |
| PLANCHE_ADDR | --addr | |
| PLANCHE_AUTH_TOKEN | --token | Fixed token (16+ characters); the app stays paired across restarts |
| PLANCHE_BODY_LIMIT | | Max request body, default 50mb (large SQL scripts) |
| PLANCHE_SQLITE_DIR | | Only SQLite files inside this directory may be opened |
| PLANCHE_SSH_CONFIG | ~/.ssh/config | SSH client config used to resolve jump-host aliases; none disables reading it and ~/.ssh/id_* |
Examples
Keep the same token between restarts so the app never needs re-pairing:
PLANCHE_AUTH_TOKEN=$(openssl rand -base64 24) npx planche # at least 16 charactersRun on another port and print the link only:
npx planche --port 9000 --no-openRestrict SQLite access to one folder:
PLANCHE_SQLITE_DIR=~/databases npx plancheHow it works
The agent is a small HTTP server with a handful of JSON endpoints
(/connect, /query, /disconnect, /test-connection, /health). The app
calls them with the bearer token from the pairing link; the agent keeps the
database connections open and forwards queries with the matching driver
(mysql2, pg, better-sqlite3).
- MySQL / MariaDB — switching databases uses
USE, so open transactions and session variables survive. - PostgreSQL — one connection is bound to one database; the "database"
you pick in the app is a schema of it and is applied with
SET search_path. - SQLite — opens a file on your disk by path. By default only files under
your home directory are allowed, excluding hidden directories (
~/.ssh, …) and~/Library/AppData(browser cookie stores live there); new files must have a.db/.sqlite/.sqlite3extension, andATTACH DATABASEis refused. SetPLANCHE_SQLITE_DIRto open files elsewhere (then only that directory is allowed),PLANCHE_SQLITE_ATTACH=1to allowATTACH, andPLANCHE_SQLITE_URI=1forfile:URIs. (Small SQLite files can also be opened directly in the browser without the agent.) - SQL Server — same model as PostgreSQL: the connection is bound to one
database and the app's "database" is a schema (
dbo, ...).USEworks in the editor because every statement runs on the same session; scripts may containGOlines. - Oracle — thin-mode driver, no Instant Client needed. Schemas (users) are
the app's "databases", switched with
ALTER SESSION SET CURRENT_SCHEMA. PL/SQL blocks end with a/line as in SQL*Plus. - SSH tunnel — MySQL, PostgreSQL, SQL Server and Oracle profiles can go through an SSH jump
host (password, private key, or the local
ssh-agent). The agent opens the tunnel withssh2on a loopback port and points the driver at it. The host may be aHostalias from~/.ssh/config:HostName,Port,UserandIdentityFileare applied like thesshcommand does (ProxyJumpis not supported), and with no key given~/.ssh/id_*are tried. An unknown host key is shown in the app (type and SHA256 fingerprint) and the connection proceeds only after you accept it; accepted keys are recorded in~/.planche/known_hosts(plain, pattern and hashed entries in~/.ssh/known_hostsare honoured too). A changed key refuses the connection until the stale entry is removed.
When planche.dev cannot be reached, the agent serves the copy of the app it
ships with (at its own address) instead of opening a browser error page, so it
keeps working offline. --serve-app forces that from the start and
--no-serve-app turns it off.
That makes the agent usable on networks with no way out. Only installing it
needs the outside world — npm i -g planche where npm is reachable, or
npm pack planche for a tarball to carry in or push to an internal registry.
From then on the served page makes no requests outside the machine at all: the
editor, the SQLite engine and the icons are part of the package, and there is
no CDN, web font or analytics. Hosted databases (Neon, Supabase, D1), SSH to
unreachable hosts and the sample database are what you lose.
Gateway mode
Run one agent on a shared host so a team can use Planche with just a browser, without anyone holding database credentials:
npx planche --gateway gateway.json --addr 0.0.0.0 --allow-remote \
--token "$(openssl rand -base64 24)" --public-url https://sql.example.comgateway.json lists named profiles; the credentials never leave this file:
{
"name": "Acme staging",
"profiles": [
{
"name": "Shop (read-only)",
"driver": "mysql",
"host": "db.internal",
"port": 3306,
"user": "reporting",
"password": "…",
"database": "shop"
},
{
"name": "Shop (writable)",
"readOnly": false,
"driver": "postgres",
"host": "pg.internal",
"port": 5432,
"user": "app",
"password": "…",
"database": "shop"
}
]
}name is the gateway's own name and profiles[].name is what browsers pick;
every other field is what the connection form asks for, under the same names
(serviceName for Oracle, file for SQLite, accountId / databaseId /
apiToken for D1, and an ssh block where you need one).
A profile behind a jump host carries that ssh block; host/port are then
resolved on the jump host, so they are usually loopback:
{
"name": "Shop via bastion (read-only)",
"driver": "mysql",
"host": "127.0.0.1",
"port": 3306,
"user": "reporting",
"password": "…",
"database": "shop",
"ssh": {
"host": "bastion.example.com",
"port": 22,
"user": "deploy",
"auth": "key"
}
}With auth: "key" and no privateKey, the agent uses the IdentityFile from
this machine's ~/.ssh/config (falling back to ~/.ssh/id_*), so the key
never lands in the config file; auth: "agent" uses SSH_AUTH_SOCK. A
privateKey you do set is the key contents, not a path. The jump host's key
must already be in a known_hosts file here, or be pinned in the profile with
acceptHostKey — in gateway mode the browser sends only a profile name and
cannot answer the unknown-host-key prompt.
- Browsers only ever send
{ "profile": "Shop (read-only)" };/connectbodies with credentials are refused in gateway mode. - Profiles are read-only by default: the agent refuses anything but a
single
SELECT/WITH … SELECT/SHOW/DESCRIBE/EXPLAINper request (statement filter), and additionally puts the session in read-only mode where the engine supports it (MySQL, PostgreSQL, SQLite). Set"readOnly": falseon a profile to allow writes. - The agent serves the web app itself at
/(--serve-app, on by default in gateway mode;--app-dirserves another build) so the app talks to its own origin — the strict CSP stays intact and no--originis needed. Put a TLS reverse proxy in front and pass its URL as--public-url; the printed pairing link is single-use, so share the token (--print-token) with teammates and let them paste it in the connect dialog's Configure box.
Security
- Every request except
GET /healthandPOST /pairrequires the bearer token. The pairing link carries a single-use code (valid 10 minutes) that the app exchanges for the token, so the token itself never appears in a URL. Repeated wrong tokens lock authentication for 30 seconds. - Browser requests are accepted only from the Planche app origin
(
https://planche.dev). Any otherOriginis refused with403, even with a valid token, and requests whoseHostheader is not the loopback address are refused (DNS rebinding). - The agent binds to
127.0.0.1by default, so only the browser on the same machine can reach it. It speaks plain HTTP, so binding to another address requires--allow-remoteplus an explicit token, and should only be done behind a TLS reverse proxy or an SSH port forward. - In gateway mode credentials live only in the gateway config file on the server; browsers pick profiles by name. Read-only profiles are enforced by the agent (statement filter + engine session read-only), not by the UI.
- The agent never stores credentials. They are passed per connection by the
app (including SSH passwords and private key contents) and live in memory
only while the connection is open. The only thing written to disk is the
SSH host key list in
~/.planche/known_hosts. - For SSH tunnels the agent reads
~/.ssh/configand, when the profile has no key, theIdentityFileit names or~/.ssh/id_*— exactly what thesshcommand would do, and the key never leaves the agent. SetPLANCHE_SSH_CONFIG=noneif you would rather the agent only used what the app sends.
Troubleshooting
- The app says the agent is offline — make sure
npx plancheis still running and that the port in the pairing link matches. Corporate proxies or browser extensions that blocklocalhostrequests can also cause this. - "Agent restarted, re-pair" — the agent generated a new token. Open the
new link it printed, or start it with a fixed
--token. 403 Forbidden— the request did not come fromhttps://planche.dev. Open the app from the pairing link the agent printed.- Wrong Node version — the SQLite driver ships prebuilt binaries for
Node 22+. Check with
node -v, or run withnpx -p node@22 planche.
License
All rights reserved. Not yet released under an open-source license.
