devuni / notifier-agent
Laravel agent for the Devuni Notifier platform - encrypted backups and announcements.
v1.9.0
2026-11-10 11:01 UTC
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^7.15.2 || ^8.3.1
- illuminate/console: ^12.55.0 || ^13.14
- illuminate/contracts: ^12.55.0 || ^13.14
- illuminate/http: ^12.55.0 || ^13.14
- illuminate/queue: ^12.55.0 || ^13.14
- illuminate/routing: ^12.55.0 || ^13.14
- illuminate/support: ^12.55.0 || ^13.14
- laravel/prompts: ^0.3.18
Requires (Dev)
- filament/support: ^5.11.0
- larastan/larastan: ^3.13.2
- laravel/pint: ^1.32.1
- mockery/mockery: ^1.6.15
- nunomaduro/collision: ^8.9.5
- orchestra/testbench: ^11.3.0 || ^12.0
- pestphp/pest: ^4.7.8 || ^5.3.4
- pestphp/pest-plugin-laravel: ^4.1.0 || ^5.0.1
- rector/rector: ^2.8.3
Suggests
- filament/support: Auto-injects announcement banners into FilamentPHP dashboards; not required when using a custom (SPA) dashboard.
Provides
None
Conflicts
- devuni/notifier-package: *
- nesbot/carbon: 3.12.0
Replaces
None
README
Laravel client for the Devuni Notifier control plane.
- Encrypted database and storage backups, uploaded to the control plane.
- Restore of this site's own backups.
- Heartbeat that tells the control plane the site is alive.
- Maintenance announcements in your dashboard.
Without a control plane (NOTIFIER_URL) there is nowhere to send backups. For standalone backups use spatie/laravel-backup.
Requirements
- PHP 8.4+
- Laravel 12.55+ or 13.14+
7z(p7zip-full, recommended) or the PHPzipextension- The client tools for your database:
| Driver | Backup | Restore |
|---|---|---|
mysql, mariadb |
mysqldump |
mysql |
pgsql |
pg_dump, or ysql_dump (YugabyteDB) |
psql, or ysqlsh (YugabyteDB) |
- MariaDB uses the same
mysqldumpandmysqlcommands.mariadb-dumpalone is not enough. - Database restore also needs the backup tool, for its safety dump (unless
--no-snapshot). - PostgreSQL restore: use a
psqlat least as new as thepg_dumpthat made the backup. 13.22, 14.19, 15.14, 16.10, 17.6 and 18+ also run in restricted mode; older clients log a warning.
Install
composer require devuni/notifier-agent
php artisan notifier:install
php artisan notifier:check
notifier:installwritesNOTIFIER_URL,NOTIFIER_BACKUP_CODEandNOTIFIER_BACKUP_PASSWORDto.env. It can also setNOTIFIER_RESTORE_TOKENandNOTIFIER_TRIGGER_SECRET.- Keep a copy of
NOTIFIER_BACKUP_PASSWORDoutside the server. Without it no backup can be restored. notifier:checktests the env, restore token, database, storage folders, dump tool, ZIP tools, seal, log channel, queue and whether the server answers. It doesn't test the backup code.- A new Laravel app has no
backuplog channel. Add one toconfig/logging.php, or logs go todaily. - Publish the config only to change exclusions:
php artisan vendor:publish --tag=notifier-config.
Scheduling
Pick one mode:
| Mode | For | Schedule |
|---|---|---|
| Control plane | Devuni-managed sites (default) | notifier:heartbeat only |
| Self-scheduled | Standalone sites | Backup commands and notifier:heartbeat |
- Control plane: it calls the trigger endpoint. Don't also schedule the backup commands, or every backup runs twice.
- Always schedule the heartbeat.
// routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('notifier:heartbeat')->hourly()->onOneServer(); // Self-scheduled mode only: Schedule::command('notifier:database-backup')->dailyAt('02:00')->onOneServer(); Schedule::command('notifier:storage-backup')->weeklyOn(0, '03:00')->onOneServer();
Commands
| Command | Does |
|---|---|
notifier:install |
.env wizard. On a configured site it needs --force; current values are prefilled. |
notifier:check |
Checks the setup. |
notifier:database-backup |
Dumps, encrypts, seals and uploads the database. |
notifier:storage-backup |
Same for storage/app/public. |
notifier:heartbeat |
Sends the heartbeat. |
notifier:database-restore |
Restores a database backup. |
notifier:storage-restore |
Restores a storage backup. |
Restore
php artisan notifier:database-restore --list
php artisan notifier:database-restore --dry-run
php artisan notifier:database-restore
| Option | Does |
|---|---|
--list |
Lists available backups. |
--id=N |
Restores this backup. Default: the newest. |
--dry-run |
Downloads, verifies and checks. Changes nothing. |
--force |
Skips the confirmation. Required in production. |
--accept-unverified=<sha256> |
Approves one backup without a valid seal. Needs --id. |
--no-snapshot |
Database only: skips the dump of the current database. |
- Needs
NOTIFIER_RESTORE_TOKEN. It is separate from the backup code. - The seal is checked before anything is extracted.
- No valid seal (backups before 1.9.0, sites without a password, another site's backup): refused. The refusal prints the
--idand--accept-unverifiedvalues that approve that file.--forceand config can't replace them. - Without
--id, the newest backup must have been sealed in the last 35 days. - An approved unsealed storage backup goes to
storage/app/notifier-restore/. Copy what you need by hand. - Database restore asks for the database name, then dumps the current database to
storage/app/notifier-restore/before it overwrites it. - That safety dump is deleted after success. If the import fails, MySQL and MariaDB are put back from it. PostgreSQL rolls back itself; the dump is kept if the rollback can't be confirmed.
- Storage restore adds and overwrites files. It never deletes.
- Storage restore refuses a backup that contains
.php,.htaccessor other executable or server config files. - Downloads stop at
NOTIFIER_RESTORE_MAX_DOWNLOAD_BYTES(default 20 GiB).
New deployment
- Set
.envwith the originalNOTIFIER_BACKUP_PASSWORD, the sameNOTIFIER_URLand aNOTIFIER_RESTORE_TOKEN. - Run
notifier:database-restore --forceandnotifier:storage-restore --force. - Start backups after the restore. The control plane rejects near-empty backups.
- A new password can't decrypt old backups.
- A different
NOTIFIER_URLpoints at another repository with other backups.
Configuration
Required:
| Key | Value |
|---|---|
NOTIFIER_URL |
Repository endpoint, e.g. https://notifier.devuni.cz/api/v1/repositories/123 |
NOTIFIER_BACKUP_CODE |
Token for requests to the control plane. |
NOTIFIER_BACKUP_PASSWORD |
Encrypts and seals backups. Never change it on a live site. |
Optional:
| Key | Default | Does |
|---|---|---|
NOTIFIER_RESTORE_TOKEN |
none | Enables restore. |
NOTIFIER_TRIGGER_SECRET |
the backup code | Token for the trigger endpoint. Must match the control plane. |
NOTIFIER_DATABASE_CONNECTION |
default connection | Connection to back up and restore. |
NOTIFIER_POSTGRES_DUMP_BINARY |
ysql_dump, else pg_dump |
Dump tool for pgsql. |
NOTIFIER_POSTGRES_RESTORE_BINARY |
ysqlsh, else psql |
Restore client for pgsql. |
NOTIFIER_POSTGRES_SCHEMA |
public |
Schema for unqualified excluded tables. Restore resets it. |
NOTIFIER_ZIP_STRATEGY |
auto |
auto, cli (7z) or php. |
NOTIFIER_SEAL_BACKUPS |
true |
false stops sealing new backups. |
NOTIFIER_CHUNK_SIZE |
20 MiB | Upload chunk size. Keep under 100 MB behind Cloudflare Free. |
NOTIFIER_MIN_STORAGE_BACKUP_BYTES |
102400 |
Smaller storage backups are skipped. |
NOTIFIER_QUEUE_CONNECTION |
sync |
Queue for triggered backups. Commands always run inline. |
NOTIFIER_LOGGING_CHANNEL |
backup |
Falls back to daily if the channel doesn't exist. |
NOTIFIER_ROUTES_ENABLED |
true |
Registers the trigger endpoint. |
NOTIFIER_ROUTE_PREFIX |
api/notifier |
Prefix of the trigger endpoint. |
NOTIFIER_BACKUPS_ENABLED |
true |
Reported in the heartbeat. |
NOTIFIER_HEARTBEAT_ENABLED |
true |
false makes notifier:heartbeat do nothing. |
NOTIFIER_RESTORE_MAX_DOWNLOAD_BYTES |
20 GiB | Restore download limit. 0 = none. |
NOTIFIER_RESTORE_MAX_EXTRACTED_BYTES |
10 GiB | Extracted size limit. 0 = none. |
NOTIFIER_RESTORE_DISK_FREE_MARGIN_BYTES |
256 MiB | Free space kept during extraction. |
NOTIFIER_RESTORE_MAX_COMPRESSION_RATIO |
0 (off) |
Refuses archives that compress better than this. |
Announcement keys are under Announcements.
Exclusions
Edit the published config/notifier.php:
'excluded_tables' => ['telescope_entries', 'telescope_entries_tags'], 'excluded_files' => ['.gitignore', '.gitkeep', 'temp'],
- Tables: plain names, or
schema.table(database.tableon MySQL and MariaDB). - Files: relative to
storage/app/public. - PostgreSQL: a restore drops and recreates
NOTIFIER_POSTGRES_SCHEMA. Anything in it that isn't in the backup is gone, excluded tables included. - PostgreSQL: exclude both tables of a foreign key or neither, or the restore fails.
- PostgreSQL: other schemas are not reset. If they already exist in the target, the restore fails.
Trigger endpoint
The control plane starts backups through this endpoint.
curl -X POST https://your-app.com/api/notifier/backup \ -H "X-Notifier-Token: <trigger secret>" \ -d "type=backup_database"
type:backup_databaseorbackup_storage. Anything else gets422.- Token:
NOTIFIER_TRIGGER_SECRET, or the backup code if that isn't set. - A missing or wrong token gets
403. So does every request while a required.envkey is missing; the reason is in the log. - 10 requests per hour per IP, failed ones included. Over the limit:
429. - Queue: with
NOTIFIER_QUEUE_CONNECTIONother thansync, the backup runs on that queue and the endpoint answers200at once. A queue worker must run. - One backup per type at a time. Without a queue a second one gets
429; with a queue it gets200and is not queued. - Without a queue, a failed backup returns
500with anerror_id. Details are in the log channel.
Heartbeat
- Sends agent, PHP and Laravel versions, queue connection, enabled features, disk space and last backup times.
- The control plane marks a site stale after 6 h without one. Schedule it hourly.
- Exits non-zero when the heartbeat isn't delivered.
Announcements
- On by default. Nothing is fetched until
NOTIFIER_URLis set. - Filament: a banner is added to every panel page automatically.
- Blade:
<x-notifier-announcements-notice />. - Inertia, Vue, React:
$service = app(\Devuni\Notifier\Services\AnnouncementsService::class); $service->activeAnnouncements(); // all active announcements $service->customAnnouncements('spa-banner'); // dashboard_type "custom", for one element id
- Cached. A failed fetch shows nothing and never breaks the page.
- Own markup:
php artisan vendor:publish --tag=notifier-views.
| Key | Default | Does |
|---|---|---|
NOTIFIER_ANNOUNCEMENTS_ENABLED |
true |
Turns announcements on or off. |
NOTIFIER_ANNOUNCEMENTS_CACHE_TTL |
900 |
Seconds a response is cached. |
NOTIFIER_ANNOUNCEMENTS_FAILURE_CACHE_TTL |
300 |
Seconds a failed fetch is cached. |
NOTIFIER_ANNOUNCEMENTS_TIMEOUT |
5 |
Request timeout in seconds. |
NOTIFIER_ANNOUNCEMENTS_MAX_VISIBLE |
5 |
Banners per location. 0 = all. |
NOTIFIER_ANNOUNCEMENTS_FILAMENT |
true |
Adds the banner to Filament panels. |
NOTIFIER_ANNOUNCEMENTS_FILAMENT_HOOK |
panels::content.start |
Default render hook. |
NOTIFIER_ANNOUNCEMENTS_RENDER_HOOKS |
the default hook | Comma-separated hooks an announcement's target may use. |
Security
- AES-256 archives with
0600permissions, deleted after upload. - HTTPS only. Requests carrying the token never follow redirects.
- Token compared with
hash_equals. SHA-256 per chunk and per file. - The ZIP password goes through stdin, never the process list.
- Report vulnerabilities privately, see SECURITY.md.
Links
License
MIT, see LICENSE.md. By Ludwig Tomas and contributors.