sergeybruhin / nova-postgres-tools
A Laravel Nova tool for the PostgreSQL backup and restore commands from sergeybruhin/laravel-postgres-tools
Package info
github.com/sergeybruhin/nova-postgres-tools
pkg:composer/sergeybruhin/nova-postgres-tools
Requires
- php: ^8.1
- laravel/nova: ^4.0|^5.0
- sergeybruhin/laravel-postgres-tools: ^0.3.1|^0.4.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel Nova tool for
sergeybruhin/laravel-postgres-tools:
take a backup, verify one, download one — from a page instead of an SSH session. Restoring is
deliberately not one of those things; see below.
The core package stays CLI-only and dependency-light on purpose. Nova is a paid, credentialed dependency, so putting a Nova tool inside it would make the core uninstallable for anyone without a licence and break its CI, which installs with no credentials at all. Hence two packages.
sergeybruhin/laravel-postgres-tools MIT, public, commands and services
↑ required by
sergeybruhin/nova-postgres-tools requires laravel/nova
Requirements
| PHP | 8.1+ |
| Nova | 4 or 5 |
sergeybruhin/laravel-postgres-tools |
^0.2 |
| A queue worker | with the postgres client binaries installed |
That last row is not optional. Backups and verifies run as queued jobs, never inside the web
request: a dump takes minutes, which would run past php-fpm's timeout. So the worker that picks
the job up needs pg_dump and pg_restore — the same binaries the CLI container has. If your
scheduler and your CLI live in different images, install them in both.
Installation
composer require sergeybruhin/nova-postgres-tools
Nova is not on Packagist, so this package can only be installed by an application that already
has Nova's own Composer repository configured — which every Nova application does. If you see
laravel/nova ... could not be found, that repository is missing from your composer.json, not
this package.
Register the tool in app/Providers/NovaServiceProvider.php:
use SergeyBruhin\NovaPostgresTools\PostgresTools; public function tools() { return [ (new PostgresTools)->canSee(fn ($request) => $request->user()?->isAdmin() ?? false), ]; }
Authorization
Two gates, because these actions are not equally dangerous.
| Gate | Covers | Default when undefined |
|---|---|---|
viewPostgresTools |
seeing the page and the list | allowed |
runPostgresBackups |
taking a backup, verifying one | allowed |
restorePostgresDatabase |
downloading a dump | denied |
Viewing and backing up are safe, and Nova's own authentication is already in front of them. Downloading hands someone every row, every password hash and every API token the application has, in one file — which is why it stays denied until you say otherwise:
// app/Providers/NovaServiceProvider.php Gate::define('restorePostgresDatabase', fn ($user) => $user->isOwner());
The name is a holdover from before restoring was removed from the web surface; it is kept so applications that already define it do not need to change anything.
Every filename these gates admit — download, verify — is resolved strictly inside the backup
directory before anything touches it. There is deliberately no path where a value from the
request can walk out of that directory: whoever holds a gate can only ever reach a file
pg:backups would have listed, never an arbitrary path on the server.
Restoring is CLI-only, on purpose
There is no POST /restore route, no restore button, no path from the browser to pg:restore
at all — only php artisan pg:restore, run by hand from a shell with access to the target
server. This is deliberate, not an oversight:
- A hidden button is not a control. Removing the button but leaving the route would mean anyone who could forge the same authenticated request with the same gate can still restore. The capability had to go, not just its visibility.
- Restoring is destructive and rare. A dropped-and-recreated production database is not a mistake you want one click and a confirmation dialog away; a terminal command someone has to type on purpose is the right amount of friction.
- The CLI already does everything the web version would.
pg:restoreverifies the checksum, checks the archive, reconciles row counts, and refuses to touch a production database without--forceandpostgres-tools.allow_production_restore— none of that had to be reimplemented or re-guarded for a web path that no longer exists.
What the page does
- Environment — the server it reaches, the client binaries the worker found (see below), whether their majors match, where dumps are written and whether an offsite disk is configured. The page renders itself from that descriptor, so a deployment with no binaries shows an accurate page rather than buttons that fail when pressed.
- Backups — the local directory or the offsite disk, with size, age, source database, source server, and whether each dump has the manifest that makes it verifiable.
- Run output, live — a queued run streams its output into the page a line at a time, so you
watch
pg_dumpwork rather than waiting for a wall of text at the end. Reload the page and it reattaches to the run in progress.
Run state lives in the cache, not the database
Deliberately, so the run tracker works the same way whether the command behind it can destroy its own status row or not, and so a job nothing ever picks up leaves no debris behind.
A consequence worth knowing: a job that no worker ever picks up is reported as stalled after
stalled_after_minutes rather than blocking the page forever behind something that will never
finish.
Client binaries are checked on the worker, not the web server
The overview page is rendered by whatever process serves the Nova request — commonly php-fpm —
which has no reason to carry pg_dump/pg_restore at all, since it never runs them. Only the
worker that actually processes pg:backup/pg:verify needs them. Probing locally from the web
request would report on the wrong process and permanently flag a perfectly healthy split-container
deployment as broken.
So the "Client" panel reports a cached probe taken by ProbeEnvironment, a job dispatched on the
same connection/queue as backups. GET /overview refreshes it whenever the cache entry is
missing or older than worker_probe.ttl_minutes (default 10). Until the first probe lands, the
panel falls back to a local check and says so — an honest "waiting for the worker" beats a
confident answer from the wrong process.
The HTTP surface
Everything the page talks to, under Nova's own domain and prefixed
nova-vendor/postgres-tools. Every route sits behind Nova's authentication and the tool's
Authorize middleware, and the destructive one re-checks its own gate in the controller — so a
hidden control is not the only thing standing between a viewer and a download. There is no
restore route; see above.
| Method | Path | Gate | Returns |
|---|---|---|---|
GET |
/overview |
view | environment, paths, offsite disk, gate descriptor |
GET |
/backups |
view | local and offsite listings |
POST |
/backups |
backup | queues pg:backup, returns a run id |
POST |
/backups/verify |
backup | queues pg:verify, returns a run id |
GET |
/backups/download |
download | streams one dump |
GET |
/runs |
view | recent run history |
GET |
/runs/{run} |
view | one run: state, and output so far |
The page polls /runs/{run} every two seconds while a run is active and stops the moment it
finishes, fails or goes stale. Nothing is broadcast, so there is no websocket or Echo setup to
do — the tool works on a plain Nova install.
The tool never shells out itself. Each of those POSTs queues a job that runs the artisan command, so the browser path and the SSH path go through exactly the same code, with the same guards, and a fix in the core package reaches both.
Troubleshooting
The menu item is missing. canSee() on the tool registration ran first, then
viewPostgresTools. Check the tool is actually returned from NovaServiceProvider::tools(),
and remember that a defined viewPostgresTools gate replaces the permissive default.
The page loads but every button is disabled, and the environment panel is red. The panel
reports what the queue worker found (see "Client binaries are checked on the worker" above),
so run php artisan pg:info inside the worker's container, not wherever you happen to have a
shell — a shell in the web container will look fine and tell you nothing.
The environment panel says "Waiting for the worker to check in" and never resolves. No
worker is consuming the queue ProbeEnvironment was dispatched to — the same cause as a stalled
run below. Check the worker is running and watching the right queue.
A run sits at "queued" and then reports itself stalled. Nothing is consuming the queue the
job went to. Check the worker is running, and that it watches
NOVA_PG_TOOLS_QUEUE/NOVA_PG_TOOLS_QUEUE_CONNECTION if you set them.
A run starts and then dies immediately. The worker's container needs the client
binaries too, not just the CLI container. This is the single most common deployment mistake:
php artisan pg:info passes from the shell, and the job still fails, because the shell and
the worker are different images.
There is no restore button and I need to restore something. Correct — see "Restoring is
CLI-only, on purpose" above. Run php artisan pg:restore <file> from a shell with access to the
target server. It is refused in production unless postgres-tools.allow_production_restore is
set; that refusal is the CLI command's, not this tool's.
The page renders unstyled or throws in the console after an upgrade. dist/js/tool.js is
stale. Rebuild it: npm install && npx mix --production.
Configuration
php artisan vendor:publish --provider="SergeyBruhin\NovaPostgresTools\Providers\NovaPostgresToolsServiceProvider" --tag=config
| Config key | Env var | Default |
|---|---|---|
queue.connection |
NOVA_PG_TOOLS_QUEUE_CONNECTION |
app default |
queue.queue |
NOVA_PG_TOOLS_QUEUE |
app default |
run_ttl_minutes |
NOVA_PG_TOOLS_RUN_TTL |
1440 |
run_history |
NOVA_PG_TOOLS_RUN_HISTORY |
25 |
stalled_after_minutes |
NOVA_PG_TOOLS_STALLED_AFTER |
15 |
cache_store |
NOVA_PG_TOOLS_CACHE_STORE |
app default |
worker_probe.ttl_minutes |
NOVA_PG_TOOLS_WORKER_PROBE_TTL |
10 |
Give backups their own queue if your default queue is busy — a dump that waits behind a hundred emails is a backup that did not happen at 03:00.
Building the assets
dist/ is committed, so installing the package needs no Node. To change the front end:
npm install npx mix --production
Nova registers Head, Heading, Card and DefaultButton globally and exposes Vue as a
global, so the tool imports nothing from Nova's own JS — the build needs no Nova checkout and
the bundle stays around 16 KB. package-lock.json is committed to the repository (though kept out of the
Composer dist, where nothing builds anything) so the built asset is reproducible, and CI fails a front-end change that was merged without rebuilding dist/.
Changelog
See CHANGELOG.md for what changed in each release. This package follows semantic versioning; while the version is below 1.0.0, routes, config keys and the run payload may change in a minor release.
Contributing
See CONTRIBUTING.md.
Security
A dump this tool can hand someone is the whole database. If you find a vulnerability — anything that lets a user reach a backup, a download or a restore they should not — please report it as described in SECURITY.md rather than opening a public issue.
Credits
License
The MIT License (MIT). Please see License File for more information.