Skip to content

Repository files navigation

Devuni Notifier Agent

Latest Version on Packagist Tests PHP 8.4 | 8.5 Laravel 12 | 13 License: MIT

Encrypted database & storage backups for Laravel apps, shipped to the Devuni Notifier central server. AES-256 ZIPs, chunked HTTPS upload, token auth, queue support. Supports MySQL, MariaDB, PostgreSQL, and YugabyteDB (via YSQL).

How it works

┌─────────────────────┐        encrypted ZIP         ┌─────────────────────┐
│  Your Laravel app   │  ───── chunked upload ─────▶ │  notifier.devuni.cz │
│  (this package)     │        (X-Notifier-Token)    │  (central server)   │
└─────────────────────┘                              └─────────────────────┘
         │                                                     │
         │ mysqldump + storage/app/public                      │ stores + monitors
         │ → AES-256 ZIP                                       │ → sends alerts
         ▼                                                     ▼
    local temp file                                     long-term backup archive
    (cleaned up after upload)

Heads up: This is the client side of the Devuni Notifier platform. Without a central server configured via NOTIFIER_URL, there's nowhere to send backups. If you don't have it, try spatie/laravel-backup instead.

Install

composer require devuni/notifier-agent
php artisan vendor:publish --tag="notifier-config"
php artisan notifier:install   # interactive .env wizard
php artisan notifier:check     # verify setup (env, DB, 7z, mysqldump, URL)

Requirements: PHP 8.4+, Laravel ^12.55 or ^13.14, the right dump tool for your DB (mysqldump / mariadb-dump for MySQL & MariaDB, pg_dump or ysql_dump for PostgreSQL & YugabyteDB), and p7zip-full (recommended) or PHP zip extension.

Migrating from devuni/notifier-package

This package is the successor of devuni/notifier-package - same namespace (Devuni\Notifier\), same env vars (NOTIFIER_*), same artisan commands, same config/notifier.php, same route prefix. The swap is one step:

composer remove devuni/notifier-package
composer require devuni/notifier-agent

Nothing else changes - your published config, .env, and scheduled notifier:* commands keep working as-is. Don't be confused by the version number: notifier-agent restarts at 1.0.0, but it contains everything notifier-package 2.8.0 had (and newer).

Never install both packages side by side - they share the Devuni\Notifier\ namespace. Composer refuses the combination (a conflict rule guards it), so if you see a conflict error during composer require, run the composer remove step first.

Heads-up when coming from notifier-package ≤ 2.7.x: version 2.8.0+ introduced announcements, which are enabled by default (see below). If you don't want the banner, set NOTIFIER_ANNOUNCEMENTS_ENABLED=false.

Usage

Scheduled backups

There are two ways to run backups on a schedule - pick one:

Managed by the control plane (default for Devuni-managed sites). The Notifier server triggers backups remotely through the HTTP API at the frequency configured in the repository settings on the control plane. Nothing to schedule on the site: keep the package routes enabled (NOTIFIER_ROUTES_ENABLED=true, the default) and the trigger authenticated (NOTIFIER_TRIGGER_SECRET, or the backup code in single-secret mode). Do not also schedule the backup commands locally - every backup would run twice.

Self-scheduled (standalone or legacy projects). Only when the control plane does not trigger this site, schedule the commands yourself - routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('notifier:database-backup')->dailyAt('02:00')->onOneServer();
Schedule::command('notifier:storage-backup')->weeklyOn(0, '03:00')->onOneServer();

In both modes schedule the heartbeat - the control plane never triggers it, the agent pushes it (see Heartbeat):

Schedule::command('notifier:heartbeat')->hourly()->onOneServer();

On demand

php artisan notifier:database-backup
php artisan notifier:storage-backup

Restore & bootstrap

Pull this site's own backups back down from the control plane - for bootstrapping a fresh deployment, disaster recovery, or refreshing staging:

php artisan notifier:database-restore --list   # see what is available
php artisan notifier:database-restore          # apply the newest database backup
php artisan notifier:storage-restore           # apply the newest storage backup

Both commands support --list, --id=, --dry-run (download + verify without applying) and require --force in production; the database restore additionally asks you to type the database name to confirm and dumps the current database to a pre-restore snapshot first (skip with --no-snapshot). Storage restore is additive - it overwrites files with matching paths but never deletes anything.

Restore is authenticated with a separate NOTIFIER_RESTORE_TOKEN (issued per repository on the control plane) - never the backup code - so leaking the credential that uploads backups cannot be used to download them. Without the token the restore commands are disabled. notifier:install offers to configure it, and notifier:check warns when it is missing.

Bootstrapping a fresh deployment: deploy → configure .env → run both restore commands (with --force on production) → done. Three things to know:

  • Copy the original NOTIFIER_BACKUP_PASSWORD from the old deployment - every archive on the control plane is encrypted with it, so a freshly generated password cannot decrypt anything. Set NOTIFIER_RESTORE_TOKEN alongside it.
  • Point the new deployment at the same repository NOTIFIER_URL as the old one - backup history belongs to the repository, a new repository id has nothing to pull.
  • Don't run the backup commands before the site has data. The control plane rejects near-empty uploads ("Backup too small"), so a database backup of a freshly migrated, empty database is expected to fail - restore first, and let the scheduled backups (the control plane's triggers, or your cron in self-scheduled mode) take over once there is something to back up.

HTTP API

The inbound trigger endpoint - this is what the control plane's scheduled backup services call; an external scheduler can use it the same way. Rate-limited to 10 req/hour.

curl -X POST https://your-app.com/api/notifier/backup \
  -H "X-Notifier-Token: your-token" \
  -d "type=backup_database"   # or backup_storage

The token is the site's NOTIFIER_TRIGGER_SECRET; when no dedicated trigger secret is configured, the backup code is accepted instead (single-secret mode).

On failure the response returns an opaque error_id (UUID) - the full detail (stack trace, mysqldump/7z stderr) stays in your backup log channel. Grep logs for the UUID to correlate.

Announcements

Pull this site's maintenance/announcement notices from the central server and show them in your dashboard. On by default - it costs nothing until NOTIFIER_URL is set (the service no-ops without a target). Disable with NOTIFIER_ANNOUNCEMENTS_ENABLED=false.

Filament hosts (the common case): when Filament is installed, the notices are auto-injected as a banner into every panel page via a render hook - nothing to place. Move the default spot with NOTIFIER_ANNOUNCEMENTS_FILAMENT_HOOK (default panels::content.start; also panels::body.start, panels::topbar.end), or turn the auto-injection off with NOTIFIER_ANNOUNCEMENTS_FILAMENT=false. Works across Filament v3/v4/v5.

Per-announcement placement: each announcement carries a target from the control plane. A Filament announcement is routed to the render hook named in its target; with no target it falls to the default hook above. The package only injects at the hooks you opt into, so to support more than one position list them all (comma-separated) in NOTIFIER_ANNOUNCEMENTS_RENDER_HOOKS - the default hook is always included:

NOTIFIER_ANNOUNCEMENTS_RENDER_HOOKS="panels::content.start,panels::topbar.end"

Existing setups need no change: with no target (or older servers) every announcement keeps rendering at the default hook exactly as before.

Banner cap: each render location shows the top NOTIFIER_ANNOUNCEMENTS_MAX_VISIBLE announcements (default 5, priority-ordered) plus a muted + N dalších oznámení overflow line; set it to 0 for unlimited. Custom (SPA) hosts via customAnnouncements() always get the full list.

Blade hosts: drop the component anywhere:

<x-notifier-announcements-notice />

Inertia / Vue / React (custom SPA) hosts: the service is framework-agnostic - render it yourself. Announcements aimed at a custom dashboard arrive with dashboard_type custom and a target DOM element id; fetch just those and mount each at its target:

// All active announcements (any dashboard_type):
app(\Devuni\Notifier\Services\AnnouncementsService::class)->activeAnnouncements();

// Only the custom (SPA) ones, optionally for a single element id:
app(\Devuni\Notifier\Services\AnnouncementsService::class)->customAnnouncements();
app(\Devuni\Notifier\Services\AnnouncementsService::class)->customAnnouncements('spa-banner');
// e.g. share as an Inertia prop, then render each item into the element named by its 'target'.

Requests are per-repository and reuse your existing NOTIFIER_URL + X-Notifier-Token (GET {NOTIFIER_URL}/announcements), so the server returns only this site's announcements - no other repositories are disclosed. Responses are cached (NOTIFIER_ANNOUNCEMENTS_CACHE_TTL, default 900 s) so the dashboard never blocks on a live request, and any fetch failure renders nothing rather than breaking your dashboard. Customize the Blade markup with vendor:publish --tag="notifier-views".

Heartbeat

Tell the control plane this site is alive and report its identity. On by default - schedule the command and the server knows the agent is running; the server marks an agent stale after 6 h of silence, so the operator sees when a site stops checking in. Disable with NOTIFIER_HEARTBEAT_ENABLED=false (the push becomes a no-op).

The host app schedules it - hourly is the recommended cadence (comfortably inside the 6 h stale window):

use Illuminate\Support\Facades\Schedule;

Schedule::command('notifier:heartbeat')->hourly()->onOneServer();

Or run it on demand:

php artisan notifier:heartbeat

Each heartbeat POST {NOTIFIER_URL}/heartbeat (per-repository, same X-Notifier-Token) reports an identity + liveness manifest: the agent / PHP / Laravel versions, the queue connection, which features are enabled (announcements, backups, heartbeat), free + total disk bytes on the storage volume, the last database & storage backup times (recorded automatically after any successful backup upload - the artisan commands, the inbound trigger and queued jobs alike), and the agent's own clock (reported_at). The server stamps its own receipt time - the agent never sends that. Unlike announcements, the heartbeat is a push: a rejected or unreachable server makes the command exit non-zero (and log to the backup channel), so your scheduler's failure handling can react.

Configure

Minimum .env:

NOTIFIER_BACKUP_CODE=...                                        # auth token
NOTIFIER_URL=https://notifier.devuni.cz/api/v1/repositories/123 # your endpoint
NOTIFIER_BACKUP_PASSWORD=...                                    # ZIP password

Optional credentials: NOTIFIER_RESTORE_TOKEN (enables the notifier:*-restore commands; issued per repository on the control plane), NOTIFIER_TRIGGER_SECRET (dedicated inbound trigger secret - without it the backup code doubles as the trigger secret; the control plane must be configured with the same value).

Other options: NOTIFIER_LOGGING_CHANNEL, NOTIFIER_ROUTES_ENABLED, NOTIFIER_ROUTE_PREFIX, NOTIFIER_ZIP_STRATEGY (auto/cli/php), NOTIFIER_CHUNK_SIZE, NOTIFIER_MIN_STORAGE_BACKUP_BYTES, NOTIFIER_QUEUE_CONNECTION, NOTIFIER_DATABASE_CONNECTION, NOTIFIER_POSTGRES_DUMP_BINARY, NOTIFIER_POSTGRES_SCHEMA, NOTIFIER_HEARTBEAT_ENABLED. See config/notifier.php for defaults and descriptions.

Database engine

The package auto-detects which dump tool to use from your Laravel connection driver:

Driver Tool used Install
mysql, mariadb mysqldump apt install mysql-client or mariadb-client
pgsql (PostgreSQL) pg_dump apt install postgresql-client
pgsql (YugabyteDB) ysql_dump if installed, else pg_dump YugabyteDB tools

By default the package backs up your app's default Laravel connection (config('database.default')). Override with NOTIFIER_DATABASE_CONNECTION=pgsql if you want a different one.

For PostgreSQL/Yugabyte, force a specific binary via NOTIFIER_POSTGRES_DUMP_BINARY=ysql_dump (or pg_dump). Non-public schemas: set NOTIFIER_POSTGRES_SCHEMA=myschema.

Exclusions

Arrays - edit config/notifier.php:

'excluded_tables' => ['telescope_entries', 'sessions', 'cache', 'jobs', 'failed_jobs'],
'excluded_files'  => ['.gitignore', 'temp', 'logs/debug.log'],

Queue offloading

API-triggered backups can be offloaded to avoid PHP timeouts:

NOTIFIER_QUEUE_CONNECTION=redis   # or database, sqs, beanstalkd

Artisan commands always run synchronously regardless of this setting.

Security

  • At rest: AES-256 encrypted archives with 0600 permissions, cleaned up after upload
  • In transit: HTTPS-only, hash_equals token comparison, per-chunk + full-file SHA-256 verification
  • No leaks: ZIP password passed via stdin (not argv - invisible to ps / /proc/*/cmdline); API errors return opaque UUIDs, not raw exception messages
  • Report vulnerabilities: see security policy - don't open public issues

Links

Credits

License

MIT - see LICENSE.md.

About

Devuni Notifier agent - encrypted backups & announcements client for the Notifier control plane (formerly devuni/notifier-package)

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages