HoneyNotify Docs

Push SaaS v1 deployment

Target topology

1. DNS / public services

Recommended names:

Put public web names behind Cloudflare when account access is available. Do not proxy or expose Redis/MySQL. The current production host binds MariaDB to 127.0.0.1 and Redis to loopback while workers are co-located.

GitHub Actions deployment

Every merge to main runs .github/workflows/deploy.yml. It builds locked Composer dependencies and static documentation, uploads an immutable release, preserves shared .env and installer state, updates the current release symlink, enforces HTTPS/security headers on all three public hosts, and checks the installer or API health endpoint.

Production paths are:

The repository secret DEPLOY_SSH_KEY must contain the dedicated private key. Its public key belongs to the honeynotify cPanel account, which requires shell access. A narrow sudo rule permits only restarting the honeynotify Supervisor group after a worker release. The workflow deliberately preserves cPanel-managed .htaccess, php.ini, .user.ini, cgi-bin, and ACME files.

On the first deployment, retrieve the token with cat /home/honeynotify/platform/shared/storage/install.token, then open https://app.honeynotify.com/install.php. The token is deleted after installation.

2. WHM/cPanel server

Use PHP 8.2+ (8.3 preferred) and MariaDB/MySQL. Create a dedicated cPanel account or document root for the SaaS rather than mixing application files into an unrelated site.

Database users

Create two DB users:

Do not use the MySQL root user from either application. The current production worker uses honeynotify_worker@localhost, with table-level SELECT, INSERT, and UPDATE grants limited to delivery processing; schema migration and dashboard privileges remain with the application account.

Install web application

cd /home/ACCOUNT/pushapp
composer install --no-dev --optimize-autoloader
cp .env.example .env
chmod 600 .env
mysql push_saas < database/schema.sql

Set the cPanel domain document root to webapp/public, not the project root.

Generate the shared encryption key once:

php -r 'echo "base64:".base64_encode(random_bytes(32)).PHP_EOL;'

Store exactly the same APP_KEY on web and worker servers. This key protects APNs/FCM credentials at rest. It should not be committed to Git.

Scheduler

Run once per minute using cPanel Cron:

* * * * * /usr/local/bin/php /home/ACCOUNT/pushapp/bin/schedule.php >> /home/ACCOUNT/logs/push-scheduler.log 2>&1
*/5 * * * * /usr/local/bin/php /home/honeynotify/platform/current/bin/process-privacy.php >> /home/honeynotify/logs/privacy.log 2>&1
30 3 * * * /usr/local/bin/php /home/honeynotify/platform/current/bin/retention.php >> /home/honeynotify/logs/retention.log 2>&1
15 2 * * * /home/honeynotify/platform/current/scripts/backup.sh /home/honeynotify/platform/current >> /home/honeynotify/logs/backup.log 2>&1

Set a strong BACKUP_ENCRYPTION_KEY, an off-site BACKUP_DIRECTORY, and retention in the shared .env. Every backup is encrypted before writing and immediately decrypted through gzip -t; periodically restore into an isolated database to test application-level recovery.

3. Private network

Create a WireGuard tunnel between the servers. Example conceptual config:

WHM:

[Interface]
Address = 10.50.0.1/24
PrivateKey = WHM_PRIVATE_KEY
ListenPort = 51820

[Peer]
PublicKey = PUSH01_PUBLIC_KEY
AllowedIPs = 10.50.0.2/32
Endpoint = PUSH01_PUBLIC_IP:51820
PersistentKeepalive = 25

push-01:

[Interface]
Address = 10.50.0.2/24
PrivateKey = PUSH01_PRIVATE_KEY
ListenPort = 51820

[Peer]
PublicKey = WHM_PUBLIC_KEY
AllowedIPs = 10.50.0.1/32
Endpoint = WHM_PUBLIC_IP:51820
PersistentKeepalive = 25

Firewall policy should allow Redis 6379 only from 10.50.0.1 and MySQL 3306 on WHM only from 10.50.0.2 plus localhost/required cPanel paths.

4. push-01

Recommended initial VM: Ubuntu 24.04, 8 vCPU, 16 GB RAM, 100+ GB NVMe.

Copy worker/ to the server and run:

sudo ./scripts/install-ubuntu.sh
cd /opt/pushsaas-worker
sudo -u pushworker cp .env.example .env
sudo chmod 600 .env

Fill .env using the private MySQL address 10.50.0.1 and the same APP_KEY as the WHM server.

Redis hardening

Bind Redis only to localhost and WireGuard:

bind 127.0.0.1 10.50.0.2
protected-mode yes

Use Redis ACLs with separate users. Example commands executed in redis-cli after choosing strong passwords:

ACL SETUSER default off
ACL SETUSER honeynotify_web reset on >WEBAPP_LONG_RANDOM_PASSWORD ~pushsaas:* -@all +ping +xadd +zcard +xlen +xpending +xrange +incr +expire +del
ACL SETUSER honeynotify_worker reset on >WORKER_LONG_RANDOM_PASSWORD ~pushsaas:* -@all +ping +xgroup +xautoclaim +xreadgroup +xack +xdel +xadd +zadd +zscore +zrangebyscore +zrem +eval
ACL SETUSER honeynotify_admin reset on >ADMIN_LONG_RANDOM_PASSWORD ~* +@all
ACL SAVE

The web server can enqueue Streams jobs, read queue health, and maintain API/authentication rate-limit counters. Workers consume acknowledged Streams, safely ignore Redis 6/7 deleted-entry tombstones, reclaim abandoned messages, atomically promote retries from a sorted set into Streams, and write terminal failures to pushsaas:dead-letter. Keep the admin credential root-only for operational inspection.

5. Worker services

Start more than one process on the same first worker node. A useful launch configuration is:

systemctl enable --now push-worker@push-transactional
systemctl enable --now push-worker@push-high
systemctl enable --now push-worker@push-normal
systemctl enable --now push-worker@push-bulk

To add concurrency, create additional systemd instances by extending the unit to accept a unique consumer suffix or run multiple templated copies. For v1, start 1-2 processes per queue and increase after load testing.

On the current cPanel host, worker/supervisor/honeynotify.conf runs the same four queues under Supervisor with PHP CLI at /usr/local/bin/php. The GitHub workflow deploys versioned worker releases and invokes a narrowly permitted /usr/local/sbin/restart-honeynotify-workers hook. MariaDB and Redis are loopback-only in this topology. A dedicated Ubuntu push-01 can replace this co-located initial worker without changing the queue contract.

6. Provider credentials

Apple APNs JSON

{
  "team_id": "APPLE_TEAM_ID",
  "key_id": "APPLE_KEY_ID",
  "bundle_id": "com.customer.app",
  "environment": "production",
  "p8_private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}

Store it encrypted:

php bin/set-provider-credential.php APP_PUBLIC_ID apns apns.json

Firebase FCM JSON

{
  "project_id": "firebase-project-id",
  "service_account": {
    "type": "service_account",
    "project_id": "firebase-project-id",
    "private_key_id": "...",
    "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
    "client_email": "firebase-adminsdk@project.iam.gserviceaccount.com",
    "client_id": "...",
    "token_uri": "https://oauth2.googleapis.com/token"
  }
}

Store it with the same command using fcm.

Delete temporary JSON files immediately after importing them.

Stripe billing

Create recurring Stripe Prices matching the packages configured by a platform administrator, then place each price_... identifier on its package. Configure STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in the shared .env and register https://api.honeynotify.com/stripe/webhook for checkout.session.completed and customer.subscription.* events. Checkout creates hosted subscription sessions; existing customers manage payment methods and cancellation in Stripe's customer portal.

7. Create first API key

Create an organisation/app using the dashboard when built, or database/bootstrap-example.sql during development. Then:

php bin/create-api-key.php APP_PUBLIC_ID "Development key"

The raw API key is intentionally printed once; only its SHA-256 hash is stored.

8. Test device registration

curl -X POST https://api.example.com/v1/devices/register \
  -H 'Authorization: Bearer ps_live_REPLACE' \
  -H 'Idempotency-Key: order-123-shipped-v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "external_user_id":"customer-123",
    "platform":"ios",
    "push_token":"APNS_DEVICE_TOKEN",
    "app_version":"1.0.0",
    "locale":"en-GB",
    "timezone":"Europe/London",
    "tags":{"plan":"premium"}
  }'

9. Test notification

curl -X POST https://api.example.com/v1/notifications \
  -H 'Authorization: Bearer ps_live_REPLACE' \
  -H 'Content-Type: application/json' \
  -d '{
    "title":"Hello",
    "body":"First push through our platform",
    "priority":"transactional",
    "target":{"type":"user","external_user_id":"customer-123"},
    "data":{"screen":"home"}
  }'

The API records the notification and places only a small job on Redis. push-01 expands the audience into batches, queues each batch, and delivers each device through APNs/FCM. Reusing the same idempotency key returns the original notification rather than sending it twice.

10. Backups

11. Monitoring required before launch

At minimum monitor:

Add Sentry or equivalent for application exceptions and Uptime Kuma/Better Stack/another external service for endpoint checks.

Use https://app.honeynotify.com/status for the public status view and https://api.honeynotify.com/health for machine-readable database, Redis, queue depth, pending work, oldest-job age, retry and dead-letter checks.