Tenant Backups
Every SaaS tenant gets the same cloud-backup feature that ships with standalone ProxPanel: scheduled and on-demand backups of their schema, encrypted with a tenant-specific key, stored in a per-tenant namespace on the license server’s backup store. A SaaS tenant cannot see, list, download, or decrypt any other tenant’s backups — the isolation is enforced both at the API and at the storage layer.
This page documents how the SaaS-specific bits work. For the in-tenant Backups UI itself, see Backups — it’s identical to the standalone experience.
What gets backed up
Section titled “What gets backed up”A tenant backup is a full pg_dump --schema=tenant_<slug> of that tenant’s schema, plus a snapshot of /uploads/tenant_<id>/ (logo, login background, favicon, generated PDFs). The backup is bundled into a single .proisp.bak file using the V2 encrypted backup format documented in Backups.
A tenant backup does not include:
- The
publicschema (tenant registry, super-admin audit log, SaaS billing) — those are super-admin concerns and backed up separately. - Other tenants’ schemas — even on the same Postgres instance.
- The host’s
docker-compose.yml, nginx config, or/etc/letsencrypt/— host configuration is the operator’s responsibility.
The per-tenant cloud namespace
Section titled “The per-tenant cloud namespace”Tenant backups are uploaded to the license server’s cloud storage at:
cloud_backups/tenant_<id>/<filename>.proisp.bakThe tenant_<id> path component scopes the namespace. The license server’s backup API enforces this: a tenant authenticated by its LICENSE_KEY + X-Hardware-Id can only list, download, and delete objects under its own prefix.
The database side mirrors this — the cloud_backups table on the license server has a tenant_id column. Every backup row carries the tenant ID; every API call filters by it. There is no cross-tenant query path.
Encryption
Section titled “Encryption”Tenant backups use the same V2 encryption format as standalone backups (see the Password Encryption page for the cipher choice). What differs:
| Component | Standalone | SaaS |
|---|---|---|
| Encryption key derivation | sha256(license_key + db_password) | sha256(license_key + tenant_id + tenant_db_password) |
| Backup header | PROXPANEL_ENCRYPTED_BACKUP_V2\nLICENSE_KEY=... | PROXPANEL_ENCRYPTED_BACKUP_V2\nLICENSE_KEY=...\nTENANT_ID=... |
| Decryption requires | The license key + the cluster’s DB password | The license key + the tenant ID + the cluster’s DB password (super-admin) OR the tenant’s own per-tenant key (in-tenant restore) |
The result: even if an attacker exfiltrates one tenant’s backup file plus the cluster’s license key, they cannot decrypt another tenant’s backup of the same cluster. The tenant ID is part of the key derivation.
Retention and quota
Section titled “Retention and quota”Each tenant has a quota and a retention policy, defined by their plan:
| Plan | Free storage | Max retention | Beyond-quota behaviour |
|---|---|---|---|
trial | 100 MB | 7 days | New backups rejected with 507 Insufficient Storage |
starter | 1 GB | 30 days | Same |
pro | 10 GB | 90 days | Same |
custom | per-contract | per-contract | per-contract |
When a tenant’s backup pushes them over quota, the new backup is not uploaded — the schedule run reports a soft failure in the tenant’s UI and the cluster admin gets an alert in the super-admin console. Old backups are not auto-deleted to make room; tenants must delete manually or upgrade their plan.
Retention is enforced by a daily cleanup job (03:30 cluster time) that drops backup rows older than plan.max_retention_days and deletes the corresponding object from the license server’s store. A backup pinned by a super-admin (rare, for support investigations) is exempt.
Tenant backup UI
Section titled “Tenant backup UI”The tenant’s Backups page is at https://<slug>.saas.proxrad.com/backups. It looks identical to the standalone Backups page. The differences are:
- The storage indicator at the top shows the tenant’s quota usage, not the host’s free disk space.
- The restore from another server option is grayed out unless the tenant has the
backups.restore_from_other_tenantpermission, which by default no one has — see the warning below. - The scheduled-backup options are restricted to daily, weekly, and monthly (no per-hour) on
trialplans.
Schedules
Section titled “Schedules”Per-tenant schedule rows live in tenant_<slug>.backup_schedules (the same table the standalone install uses). The cluster has one scheduler service that wakes every minute, iterates every tenant’s schedule table, computes “is now within this schedule’s run window in this tenant’s timezone?”, and queues backup jobs accordingly.
Crucially, each schedule honours its tenant’s system_timezone, not the host’s. Acme in Asia/Beirut and BigISP in America/New_York both running a 02:00 daily backup will fire at the right local time for each — see the v1.0.75 / v1.0.95 timezone fixes in Audit Findings & Hardening.
Restore on the same cluster
Section titled “Restore on the same cluster”- Tenant admin goes to Backups in their panel.
- Picks a backup from the list.
- Clicks Restore.
- Confirms in the modal (this drops every table in
tenant_<slug>and re-creates from the backup). - The cluster’s API container:
- Downloads the encrypted backup from the license server.
- Decrypts using the tenant’s derived key (no manual key entry).
- Pipes the decrypted SQL into
psql -d proxpanel_saaswith--single-transactionand aSET search_path TO tenant_<slug>prelude.
- Frontend warns the user to log out and back in (their sessions reference IDs that may have changed).
The whole restore takes about 10 seconds per 1,000 subscribers on warm hardware. The tenant’s other admins are logged out automatically; in-flight PPPoE sessions are unaffected (RADIUS reads from the now-restored data on the next auth packet).
Cross-cluster migration
Section titled “Cross-cluster migration”A tenant who wants to move their data to a different SaaS cluster (or to a standalone install) downloads their V2 backup and contacts support:
-
From the tenant panel: Backups → Download the latest backup. The file is decryptable only by entities that hold the cluster’s license key, so the file at rest is safe to email or upload to cloud storage temporarily.
-
Support engineer on the destination cluster runs
/scripts/import-saas-tenant.sh <slug> <backup-file>:- Creates the tenant row + schema if it doesn’t already exist (using onboarding pipeline).
- Decrypts the V2 backup using the source cluster’s license key (super-admin enters it; not the tenant’s secret).
- Loads the data into the new
tenant_<slug>schema on the destination cluster.
-
Cloudflare CNAME or DNS change for any custom domain is the tenant’s responsibility.
This flow is rare but documented. For a standalone migration, the destination uses the same V2 format but with the standalone install’s license key — see Backups.
Operational alerts
Section titled “Operational alerts”The super-admin console surfaces backup health across the cluster:
| Alert | Trigger |
|---|---|
| Tenant over quota | Any tenant whose cloud_backups total bytes exceeds 100% of plan |
| Tenant backup failing repeatedly | Three consecutive failed scheduled-backup runs for the same tenant |
| Backup retention overflow | The host’s local backup-staging dir at /var/lib/proxpanel/backup-staging is over 80% full |
| License-server upload failed | API container cannot reach license.proxrad.com for upload — backups are queued locally and retried |
Local queueing lets the cluster survive transient license-server outages without losing backups. The queue is drained when the license server is reachable again. A queued-but-not-yet-uploaded backup is visible in the tenant’s UI with a “pending upload” badge.
Capacity planning
Section titled “Capacity planning”A typical SaaS tenant with 1,000 subscribers and 6 months of accounting history produces a backup of about 50–80 MB compressed. With AES-256-GCM overhead and the V2 header, the on-disk size is roughly the same. Multiply by retention to estimate cluster storage:
| Tenants × subscribers | Daily backups, 30-day retention | Total cloud storage per tenant |
|---|---|---|
| 100 tenants × 1,000 subscribers | 30 × 60 MB = 1.8 GB | ~1.8 GB / tenant, 180 GB total |
| 50 tenants × 5,000 subscribers | 30 × 300 MB = 9 GB | ~9 GB / tenant, 450 GB total |
| 10 tenants × 25,000 subscribers | 30 × 1.5 GB = 45 GB | ~45 GB / tenant, 450 GB total |
The license-server backup store sizes for the aggregate. Plan for headroom — tenants do not delete old backups voluntarily, and quota-overrun rejection happens at upload time, which surprises operators who haven’t sized correctly.
Disaster scenarios
Section titled “Disaster scenarios”| Scenario | Recovery path | Approx. RTO |
|---|---|---|
| A tenant accidentally deletes their subscriber database | Restore from the most recent automated backup, lose at most 24 hours of data | 5 minutes |
| The SaaS cluster’s Postgres data corrupts | Restore the host-level Postgres backup; tenant backups are not used (they’re per-schema) | 30 minutes |
| The whole SaaS host is lost | Stand up a fresh host, run the cluster install, restore the public.tenants schema from host-level backup, then restore each tenant’s data from cloud backups | 2–4 hours |
| A tenant claims their backup is corrupted | Try restore in a dry-run mode (super-admin-only); if confirmed corrupted, fall back to the previous day’s backup | 10 minutes |
| License server is unreachable during a scheduled backup | Backup queues locally; retried until the license server returns | n/a (transparent) |
The recovery paths assume the cluster’s host-level Postgres backup is separately managed (it is — via the host’s own pg_basebackup to off-cluster storage). Tenant cloud backups alone cannot rebuild the cluster from scratch.
Permissions
Section titled “Permissions”Backups are admin-only inside a tenant:
| Permission | Effect |
|---|---|
backups.view | See the Backups page and the list of existing backups. |
backups.create | Trigger an on-demand backup or create a schedule. |
backups.restore | Restore a backup. Destructive — confirms via modal. |
backups.delete | Delete a backup from cloud storage. Frees quota. |
backups.edit | Edit a schedule. |
On the super-admin side, saas.backups.view lets you see the cluster-wide backup health page and force-trigger or restore on a tenant’s behalf.
Related pages
Section titled “Related pages”- Backups — the in-tenant Backups page, identical to standalone.
- SaaS Overview — what is shared vs. isolated across tenants.
- Schema-Per-Tenant Isolation — what
pg_dump --schema=tenant_<slug>actually dumps. - Password Encryption — the AES-256-GCM cipher used for backup encryption.
- Super Admin Console — cluster-wide backup health and operator-level restore.