Push SaaS v1 deployment
Target topology
- WHM/cPanel server: PHP web/API, MariaDB/MySQL, dashboard, cron scheduler.
- push-01 Ubuntu server: Redis and all background push workers.
- WireGuard: private point-to-point network. Example addresses: WHM
10.50.0.1/24, push-0110.50.0.2/24. - Only HTTP(S) endpoints intended for customers are public. Redis and MySQL must never be public to the Internet.
1. DNS / public services
Recommended names:
www.example.commarketing websiteapp.example.comSaaS dashboardapi.example.compublic SDK/API endpointstatus.example.comwhen a separate public status domain is required
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:
/home/honeynotify/public_html/app.honeynotify.com— dashboard andinstall.phpentry points./home/honeynotify/public_html/api.honeynotify.com— API and Stripe webhook entry point./home/honeynotify/public_html/docs.honeynotify.com— generated static documentation./home/honeynotify/platform— releases and shared secrets/state outside public document roots./home/honeynotify/worker— versioned worker releases and shared worker environment.
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:
push_app: normal application CRUD rights.push_worker: worker-only rights required to read apps/devices/provider credentials and write batches/delivery results. Restrict its host to the WireGuard address10.50.0.2.
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
- MariaDB: daily database backup plus off-server copy.
.env/ APP_KEY: encrypted secrets backup. Losing APP_KEY means encrypted provider credentials cannot be decrypted.- Redis retains in-flight Streams, retry and dead-letter state, while campaign and recipient delivery state remains authoritative in MariaDB. Enable Redis AOF (
appendonly yes) and include it in operational recovery procedures. - Do not rely solely on cPanel account backups for the complete service.
11. Monitoring required before launch
At minimum monitor:
/healthon API- Redis memory / connected clients / queue lengths
- running systemd worker counts
- worker restarts
- notifications queued vs completed
- APNs/FCM failure rate
- invalid-token rate
- MySQL connections / slow queries / disk
- disk/RAM/CPU on both servers
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.