Supported today — PostgreSQL · MySQL · MariaDB
Back up. Restore. Mirror. Sync. Then let it run itself.
sloop registers a database once — a name, a host, a role — and then backs it up, restores it, mirrors it, syncs it and reads it, on Windows, Linux and macOS. Drive it from a menu that covers every command, or from a flag a scheduler can run. Install it as a background service and it keeps its own schedule, so there is no cron line to write and no Task Scheduler entry to forget.
One binary. No agent to deploy, no server to run, no account to make.
Your credentials never leave your machine. No telemetry, no analytics, no update pings — and nothing in the binary that could phone home.
$ curl -fsSL https://dbsloop.github.io/install.sh | shWhat sloop does
sloop backup
Backups that prove themselves
Every row counted on both sides, and a manifest filed beside every dump.
sloop restore
Put one back
Replaces what is in the database now. Typed out, never clicked, then verified.
sloop mirror
An exact copy
The destination ends up identical to the source. Anything else there is gone.
sloop sync
A merge, not a copy
Rows added and replaced, and rows only the destination had are kept.
sloop query
Read one without SQL
Pick a table, tick the columns. The server is the thing that refuses writes.
sloop service schedule
A schedule, with no cron
Nothing is written to crontab, Task Scheduler or a systemd timer.
sloop service activity
What each one has been doing
Rows in, rows out and size, by day, by week and by month.
sloop doctor
Before you trust any of it
The grants a backup role really needs, checked against the live connection.
From nothing
Three commands, and it is running itself.
Register it, back it up, put it on a schedule. There is no fourth step: the service takes every backup from then on, applies the retention policy, and prunes what falls outside it.
Real output, in real text. Select it, copy it, search it — it is the same HTML a crawler reads, whether or not you watched it arrive.
$ sloop db add shop --url postgres://app@db.internal:5432/ordersregistered shop in the project registry (a .sloop at or above the working directory), password from the OS keyring postgres://app@db.internal:5432/orders $ sloop backup shopshop postgres://app@db.internal:5432/orders✓ Connected postgres 17.9, TLS, 48 tables, 1284003 rows✓ Dumped 48 tables 432.6 MB, encrypted✓ Checked 9f3ac1d2e8b0 backed up to backups/postgres/shop/20260919T031500Z 412.6 MiB in 12.8s, sha256 9f3ac1d2e8b0 taken 2026-09-19 09:15:00 +06:00 (20260919T031500Z) $ sloop service schedule shop --every 1d --keep 7Scheduled. shop is backed up every day the newest 7 are kept, the rest are pruned no cron line, no scheduled task — the service takes it01
Not a policy. A property of the build.
Plenty of tools promise not to send your data anywhere. This one cannot. There is no HTTP client anywhere in the dependency graph — no reqwest, no hyper, no ureq — and no SSH client either, because an embedded one could open a socket to anywhere at all. Continuous integration fails the build if either ever appears, across normal, build and development edges, for every target.
The two jobs that genuinely need the network shell out to programs your machine already has: curl to fetch a client tool Windows does not ship with, and your own ssh to reach a database whose port is closed. Both only after you say yes. The background service never opens a port — not for status, not for metrics, not for a local socket bound to loopback.
There is no version check and no a new release is available. That would need HTTP, and it is not worth the claim. Here is how you check the claim yourself, in thirty seconds.
$ cd cli$ cargo tree | grep -Ei 'reqwest|hyper|ureq|curl'# nothing. no HTTP client is in this graph. $ cargo tree | grep -Ei 'ssh2|russh|libssh2'# nothing either. no socket of any kind is in it. Nothing is written to a log that could not be pasted into a public issue. No password reaches a config file, a dump filename, or ps output — not even for a moment.
- Nothing is bundled that would need fetching
mysqldumpis GPL v2 and bundling three platforms would put about 150 MB in every release, so client tools are acquired once, on demand, after you agree — and never at all without a terminal to ask at.- The claim is checked by the build, not by a promise
ci/no-http-client.shreads the graph with--target all, so a dependency hiding behind acfg(windows)is caught too.
02
A menu anyone can drive. A flag a scheduler can run.
sloop on its own opens an interactive menu, and every command is on it — six doors on the front page, with the commands one keystroke behind the one they belong to. It reimplements nothing: a screen collects exactly what a flag carries and then calls the same function the flag calls, so the two surfaces cannot drift apart.
Typing finds a command wherever it lives. mirror is not written on the front page, and typing it still lands on the door that holds it — every name behind a door is matched, not only the six you can see.
The job runs inside the screen. Every line it prints, every password it asks for and every confirmation you type is drawn in the alternate buffer — sloop does not hand your terminal back until you quit, and quitting gives you back exactly the terminal you had. ← Back is on every menu and Esc leaves any prompt. A machine that has not been set up opens on Setup and nothing else.
And because nothing a job prints survives that screen, every finished run lands on one of its own: what it did, every step it took, and the flag form of what just ran. A session you worked out by hand becomes a line you can schedule.
What is behind the six doors
Databases
sloop db …add, create, list, test, edit, rename, remove, drop
Backups
sloop backup …back one up, back all up, list, prune, restore
Copy a database
sloop mirror …mirror, sync
Look inside one
sloop query …query
Background service
sloop service …install, attach, schedule, activity, status, detach, start, stop, uninstall
This machine
sloop doctorinstall a database server, open sloop's own database, doctor, export the backup key, import one
Thirty commands under seven headings was the front page before this one, and it was a list nobody read to the end of. Six doors is a page you can take in at a glance.
$ sloop _ | | sloop v0.1.5 ___| | ___ ___ _ __ your databases, backed up and moved about / __| |/ _ \ / _ \| '_ \ \__ \ | (_) | (_) | |_) | ✓ 3 databases ✓ last backup 2h ago |___/_|\___/ \___/| .__/ ✓ service running | | C:\Users\you\.sloop · the global registry |_| ? What would you like to do?› Databases 3 registered sloop db … Backups 12 kept · newest 2h ago sloop backup … Copy a database mirror exactly, or merge with sync sloop mirror … Look inside one tables and rows, with no SQL to type sloop query … Background service running sloop service … This machine servers, the backup key, and a health check sloop doctor Quit ↑↓ to move, enter to choose, esc to go back, or type a few letters to filterThe mark sits beside the facts rather than above them, so the list below it has room to be a list. Every row is three columns — what it is called, what it is currently doing in the colour that fact deserves, and the command that does the same thing in grey on the right — and the columns are measured across the whole list rather than per row.
sloop › Backups › Back one up now ✓ DONE Back one up now 4.2s ┃ ✓ Connected postgres 17.9, 2 tables, 10601 rows ┃ ✓ Dumped 2 tables 67.6 kB, encrypted ┃ ✓ Checked 9f2a1c4e77b0 ┃ ✓ Verified exact count(*) on both sides the same thing, from a shell sloop backup shop --yes ? That is done.› ← Home A tick, a cross or a triangle on every status — green worked, red did not, amber worked and there is something to know about it. The way out of a result is ← Home and nothing else is on the screen: the thing it had just done used to sit there selected, and Enter on it did that thing again.
03
Register it once. Then just use the name.
Give sloop a URL or four fields, and from then on every command takes the name. sloop db create goes one step further and makes the database and the role that owns it on the server, with the grants that make it actually usable, then registers the result. db remove forgets a record and never touches a server; db drop destroys the database and makes you type its name.
A registry lives in your project, the way .git does — sloop walks up from wherever you are standing and uses the nearest one, stopping short of your home directory. There is a global registry too. A name in both resolves to the project's; --global forces the other, and global:shop says so for one command.
The registry is a database, not a file. sloop keeps its own state — your registrations, the service's readings, the schedules — in a PostgreSQL 18 of its own, under ~/.sloop on all three platforms, needing no administrator to set up. sloop server connection prints how to open it in psql or a GUI yourself.
Which registry a bare name means
- 1
--globalthe global registry, whichever directory you are standing in. - 2
-C <path|name>that project — a directory, or a name sloop init recorded. - 3
SLOOP_PROJECTthe same thing from the environment. -C outranks it. - 4
the nearest .sloopat or above this directory, stopping short of your home. - 5
otherwisethe global registry.
The first one that applies wins, and every command says which registry it used and why — being told the global registry when you never asked for it reads like a bug until something explains it.
$ sloop db listshop postgres://app@db.internal:5432/orders project · the OS keyringbilling mysql://svc@10.0.0.9:3306/billing global · the environment variable ${BILLING_PW}reports postgres://ro@127.0.0.1:5432/reports global · the command `op read op://vault/db/pw` through ssh://deploy@bastion.internal:22 A registration records where a password comes from and never what it is — which is what makes putting one there by accident impossible rather than discouraged. The last line is a database whose port is closed to everything outside its own server, reached through your own ssh.
04
Every backup proves that it landed.
A backup lands in its own UTC-stamped directory with a manifest beside it: engine version, size, SHA-256, duration, per-table row counts, and both timestamps with the offset. --replace keeps one copy at a fixed path a script can name instead. backup --all runs to the end and then tells you which databases failed, rather than stopping at the first one.
Verification is exact, not estimated.
Every row is counted on both sides, per table, in one statement. The planner's estimate has been observed reporting double the true count, so it is available deliberately as SLOOP_VERIFY=fast, is always labelled an estimate, and is never presented as proof.
A live source moves while a dump runs, and sloop says drift in those words rather than calling it a failure. A table that did not arrive at all is a failure, and the run exits 6 — finished, but the numbers disagree.
- Retention is a policy, not a cron job
backups prune --keep 7or--older-than 30d, and it shows you what it is about to delete before it deletes anything.- One lock per database
- An overrunning backup cannot race the next scheduled run. The second one exits
7and takes nothing. - UTC in paths, local time everywhere you look
- Directories are named
20260919T031500Zso they sort and survive a clock change. The manifest records both, plus the offset.
$ sloop backup shopshop postgres://app@db.internal:5432/orders✓ Connected postgres 17.9, TLS, 48 tables, 1284003 rows✓ Dumped 48 tables 432.6 MB, encrypted✓ Checked 9f3ac1d2e8b0 backed up to backups/postgres/shop/20260919T031500Z 412.6 MiB in 12.8s, sha256 9f3ac1d2e8b0 taken 2026-09-19 09:15:00 +06:00 (20260919T031500Z) $ sloop mirror shop --to shopcopy --confirm shopcopy copied in 2.4s▲ Verified 1 table moved — the source was live verifying — exact count(*) on both sides orders 500 → 500 line_items 1284 → 1284 audit_log 9120 → 9163 drift: the source is live 3 of 3 tables matched, 1 driftedaudit_log gained rows between the two counts, which is a live database behaving normally — so it is named as drift and the run still succeeds. A table reading — on the destination would not be.
05
No crontab line. No Task Scheduler entry. No systemd timer.
sloop registers itself with your machine's own service manager — a systemd unit on Linux, a launchd daemon on macOS, a real Windows service registered with the Service Control Manager — and does the work with nobody logged in. It takes the backup, applies the retention policy, and prunes what falls outside it, running the same commands you would have typed yourself.
01
Register it with the machine
sloop service install
A systemd unit, a launchd daemon, or a real Windows service. It comes back at boot. Needs an administrator or sudo, and says so before it touches anything.
02
Point it at a database
sloop service attach shop
Picked up at the next round, with nothing restarted. Detaching stops the readings and deletes none of what was already recorded.
03
Say how often, and how many to keep
sloop service schedule shop \ --every 1d --keep 7
It runs the same backup and prune you would have typed, prunes only after a backup that worked, and catches a missed run up once rather than a hundred times.
$ sloop service activityActivity 2 databases attached shop Today 184,220 rows in · 1,905,633 out · 1,440 readings 7 days 1,284,004 rows in · 13,402,118 out · 10,080 readings 30 days 5,610,882 rows in · 57,884,203 out · 43,200 readings Size 412.6 MiB on disk, read 2026-09-19 09:41:02 +06:00 Moved 11.8 GiB in · 0 B out · real bytes sloop moved, over 30 days billing Today no readings in this period 7 days 48,110 rows in · 211,904 out · 3,902 readings rows are what the server counted; Moved is real bytes sloop read or wrote. Nothing here is bytes on the wire — no engine reports those per database.And it tells you what it cannot measure.
An attached database is sampled every minute, rolled up by the hour, and kept in UTC — so a day, a week, a month and a year are all a sum over a range, with no rollup tables to keep in step and nothing to backfill. Rows in, rows out and size are what the engine's own counters say. Moved is real bytes, because sloop read or wrote them itself during a dump or a restore.
Bytes on the wire are not available per database on any of these three engines, so sloop says that in those words rather than drawing a number it cannot stand behind. A period with no readings in it is shown as having none, never as a zero — an idle database and an unwatched one are different facts.
A missed run — a laptop asleep at 03:00 — is caught up once and labelled late, not a hundred and sixty-eight times. A collision with a backup you started by hand exits 7 and stands aside, because the backup somebody is already taking is the backup. sloop service status says when each database last ran and when it runs next, so a schedule that has quietly stopped is visible without waiting for the backup nobody took.
06
mirror and sync are different commands.
They are the two people confuse, so here is the difference before anything else. Both are same-engine only: PostgreSQL into MySQL is refused rather than attempted, because the type systems do not map cleanly and a silent corruption is worse than a refusal. And the source of a copy is only ever read — no statement written against it changes anything, not even ANALYZE.
sloop mirror shop --to staging
An exact copy.
The destination ends up identical to the source. Anything that existed only there is gone afterwards — that is what exact means, and it is the whole difference from the other one. Dump and restore, streamed straight through, so no file is left behind; --safe keeps a temporary dump only until verification passes.
- The destination's whole shape is replaced — its indexes and constraints come from the source.
- The destination database itself, its owner and its credentials are never touched.
- Copying a database onto itself is refused — host, port and database are compared together.
sloop sync shop --to warehouse
A merge.
Rows are added, rows already there are replaced, and rows only the destination had are kept. COPY into temporary tables, then upsert in foreign-key order from a topological sort.
- A table with no primary key is skipped, and named. It is not guessed at.
- Sequences and identity columns are reset afterwards, so the next insert lands.
- Rows deleted in the source will linger in the destination, and you are told which.
- A circular foreign key is refused, with the tables in the cycle named.
Either one can make its destination first — --create builds the database and the role that owns it, then copies into it. Either can be scoped to some tables with a repeatable --table, and --with-references pulls in the tables those point at rather than refusing a child named without its parents.
07
Read a database without writing SQL.
Pick a table, tick the columns, choose the test — everything comes off a list and the only thing you type is the value you are looking for. Joins are offered from the foreign keys a table actually has, in both directions, because a key that answers who placed this order from one side answers and their orders from the other. Results open in a grid you can scroll, two hundred rows at a time.
Read-only is the server's guarantee, not a list of forbidden words. Everything it runs — including a statement you hand it with --sql — runs inside a transaction the engine has been told is read-only. An UPDATE comes back cannot execute UPDATE in a read-only transaction, and so does a data-modifying CTE that begins with the word WITH — which is the case any keyword blacklist misses.
$ sloop query shop --sql "select name, state from orders"Reading: shop project name stateAda Lovelace shippedGrace Hopper packingAlan Turing shipped Rows: 3 Building a query is a screen of questions, so with no terminal to ask at it exits 2 naming --sql — which is the rule every command in this tool follows.
08
Four ways to hold a password. None of them is a file.
No plaintext password is written anywhere — not to a config file, not to a log, not into a dump's filename, and not into ps output. Nothing is ever passed in argv; a secret reaches a child process down a pipe or through that one child's environment, never wider. Which route you get depends on the machine you are on.
By default
The OS keyring
Credential Manager on Windows, Keychain on macOS, Secret Service on Linux.
Headless Linux
An encrypted file
XChaCha20-Poly1305 with an Argon2id-derived key, for a box with no desktop session.
Automation
An environment variable
${DB_PASSWORD}is read at run time, so the registry holds the name and never the value.Teams
A password command
--password-commandreads it from your password manager. It arrives down a pipe, so it never reachesps.
Encrypted backups
The asymmetry is the point.
Backups are encrypted to an age public key that sits in the registry, so a scheduled backup at three in the morning needs no secret to unlock. The private key lives in the keyring, and only a restore ever asks for it.
The first encrypted backup refuses to run until that key has been exported somewhere else, or you have explicitly said you do not want to. Losing it loses everything, and that is said once, hard, at the only moment it can still be acted on.
Over SSH
sloop runs your ssh, rather than shipping one.
A database with no public address, or with its port closed to everything outside its own server, is reached through a local port forward sloop opens with the ssh already on your machine. Your ~/.ssh/config, your ProxyJump through a bastion, your agent, your hardware key and your known_hosts all work, because none of them has been reimplemented.
One authentication per session, not one per command: ten operations in one menu session ask once, and every forward closes when the session ends. And it is the same reason the downloads shell out to curl — an embedded SSH client could open a socket to anywhere, and the promise at the top of this page would stop being true while the check that guards it still passed.
09
Built to be run by something that is not a person.
It never asks a question it cannot ask. When standard input is not a terminal and something needs confirming, it exits with an error naming the exact flag that answers it — because a scheduled run that hangs waiting for a keystroke is the worst failure this tool could have.
--yes, --force and --confirm are three different things and none stands in for another. --yes answers questions. --force overrides a refusal and answers none. --confirm shop is the only way to run a destructive command with no terminal, and it has to match exactly — so a scheduled line names what it destroys and cannot be repointed at something else by editing one flag.
--json prints one document as the whole of standard output, so it parses. --dry-run is a real rehearsal: every command does all the reading and all the checking, then stops at the point where it would write. --log-file writes what the run printed with anything that reads like a credential taken out, so a sloop log can be pasted into a public issue.
Exit codes, frozen at 1.0
- 0
- It worked.
- 1
- Something went wrong that none of the codes below describes.
- 2
- Bad usage, an unknown name, or a question with no terminal to ask it at.
- 3
- The connection failed.
- 4
- The dump failed.
- 5
- The restore failed.
- 6
- It finished, and then the row counts did not agree.
- 7
- Another run holds the lock on that database.
- 8
- doctor found something that will break a backup.
Adding one is fine. Changing one is a breaking release — something is parsing these. 6 is the one worth wiring up: finished, but not verified, is neither success nor a crash.
10
Planned
Four things that are not built. They are here so you know where this is going, with no date attached to any of them.
Planned
Writing and saving queries
Beyond the read-only builder that
sloop queryis today.Planned
Schema changes
Creating and altering tables from sloop, with the same typed confirmation as a drop.
Planned
Editing rows in place
Finding and changing a row, for the times a dump is far too big a hammer.
Planned
A local interface
One that reads sloop's own PostgreSQL the way the CLI does. It would not be served by the service — that would need a port, and there is not going to be one.
Cross-engine copying is not on this list. PostgreSQL to MySQL has been worked through against real servers of both and is on hold: copying is same-engine, the other direction is refused rather than attempted, and nothing here promises otherwise.