SaaS Overview
ProxPanel SaaS is a multi-tenant deployment of the same ISP-management platform that ships as a standalone customer install. One cluster — one set of containers, one PostgreSQL instance, one Redis, one RADIUS pair — serves an arbitrary number of independent ISPs. Each ISP (a tenant) gets its own subdomain, its own data, its own admin users, and its own brand. They never see each other.
This page covers what runs where, what is shared between tenants and what is strictly isolated, and the operational implications of running SaaS instead of one-server-per-customer.
Where SaaS mode fits
Section titled “Where SaaS mode fits”You want SaaS mode when:
- You are an MSP / reseller signing up many small ISPs and don’t want to run one VPS per customer.
- The tenants are small enough (under ~5,000 subscribers each) that giving each one a dedicated server is wasteful.
- You want self-service signup — new tenants pick a subdomain, pay, and get a working panel in under a minute.
You want standalone mode when:
- The customer has more than ~10,000 subscribers — a dedicated host scales better and lets you tune Postgres / Redis per-customer.
- The customer’s data-residency or contractual obligations forbid co-tenancy.
- The customer wants to run on their own hardware behind their own firewall.
The single SaaS host at saas.proxrad.com is sized for many small tenants. The three production single-tenant customers (Acme, Acme ISP, plus one more) each run their own ProxPanel install on dedicated hardware.
Architecture
Section titled “Architecture” saas.proxrad.com | *.saas.proxrad.com (wildcard DNS, grey-cloud) | v +---------------+----------------+ | nginx (host: 443) | | matches subdomain → tenant | +---------------+----------------+ | v +---------------+----------------+ | proisp-api container | | TenantMiddleware sets | | schema search_path per req | +---------------+----------------+ | +---------+-----------+-----------+----------+ | | | | v v v v Postgres Redis RADIUS WireGuard (shared) (shared, (shared, (shared, keyed by tenant) shared per-tenant realms) peers) ^ | proxpanel_saas database | +----------+----------+----------+-----------+ | | | | | v v v v v tenant_acme tenant_bigisp tenant_xyz tenant_demo public (schema) (schema) (schema) (schema) (super-admin)Each tenant gets a PostgreSQL schema named tenant_<slug> (e.g. tenant_acme). The same set of tables exists inside every schema — subscribers, services, nas_devices, radacct, transactions, and so on. The connection’s search_path is set per-request to the calling tenant’s schema, so every query a tenant runs hits its own tables only.
What is shared
Section titled “What is shared”These resources are physically shared across every tenant on the cluster:
| Resource | Why it’s shared | Tenant-visible impact |
|---|---|---|
| PostgreSQL instance | One Postgres process, one connection pool, one WAL | A heavy tenant can saturate connections — the per-tenant connection limit defends against this. |
| Redis instance | Cache + sessions + rate-limit counters | Keys are namespaced by tenant_<id>:.... No cross-tenant leakage. |
| RADIUS pair | One proisp-radius container handles all PPPoE auth across tenants | Each tenant’s NAS uses a different shared secret + realm. RADIUS looks up subscribers via the NAS’s tenant routing. |
| WireGuard tunnel termination | One wg0 interface, many peer keys | Each tenant has its own peers and its own /24 allocation. |
| Host OS, Docker daemon, nginx, kernel | One physical or virtual host | Tenants share the host’s CPU / RAM / disk. Super-admin enforces tenant quotas. |
| TLS certificate | One wildcard cert for *.saas.proxrad.com | All tenant subdomains share the same cert. Custom domains get their own. |
| License server registration | The cluster itself runs in SaaS mode — license check is for the host, not per-tenant | Tenants don’t need their own ProxPanel licenses. |
What is isolated
Section titled “What is isolated”Every tenant gets its own:
| Resource | Where it lives | How it stays isolated |
|---|---|---|
| Subscriber data, RADIUS accounting, transactions, invoices | tenant_<slug> schema | search_path is set on every connection. A tenant query can only see its own schema’s tables. |
| Admin users, resellers, permission groups | tenant_<slug>.users, tenant_<slug>.resellers | JWTs carry tenant_id — a tenant’s token only authenticates against its own schema. |
| Branding (logo, primary colour, login background, favicon) | tenant_<slug>.system_preferences + /uploads/tenant_<id>/ | Uploads are stored in per-tenant subdirectories. Branding is reloaded per request based on hostname. |
| Timezone | tenant_<slug>.system_preferences.system_timezone | All scheduled jobs (daily quota reset, backups, notifications) honour the tenant’s own timezone, not the host’s. |
| Backups | cloud_backups table on the license server, keyed by tenant ID | A tenant’s backup is encrypted with that tenant’s license-derived key. Other tenants cannot decrypt it. |
| NAS devices, IP pools, services, communication rules | All inside tenant_<slug> | A tenant defines its own routers, its own service plans, its own SMS templates. |
| Audit log | tenant_<slug>.audit_logs | Each tenant has its own audit trail. The super-admin can browse across tenants from the super-admin console only. |
| Sessions / JWT secrets | One JWT signing key for the cluster, but tokens are scoped by tenant ID claim | A token issued to tenant A’s admin cannot be replayed against tenant B. |
Tenant-scoping in code
Section titled “Tenant-scoping in code”The single most important piece of SaaS code is the request-scoped tenant resolver in backend/internal/middleware/tenant.go. On every incoming request:
- Extract the hostname from the request.
- If the hostname matches
*.saas.proxrad.com, the subdomain is the tenant slug. - If the hostname is a custom domain (e.g.
panel.myisp.com), look it up in thetenants.custom_domaincolumn. - Resolve the tenant’s
schema_name(cached for 60 seconds — invalidated on signup, domain change, or tenant deletion). - Bind a tenant-scoped
*gorm.DBtoc.Locals("tenant_db")so handlers always read/write the right schema.
Handlers then call middleware.GetTenantDBFromCtx(c) instead of touching database.DB directly. In standalone mode the helper returns the global DB; in SaaS mode it returns the per-request tenant DB. The same handler code runs in both modes.
Operational model
Section titled “Operational model”Capacity per host
Section titled “Capacity per host”A 32-core / 64 GB SaaS host comfortably runs 50 tenants averaging 1,000 subscribers each — 50,000 subscribers in total. The architecture is identical to a standalone install, so the per-subscriber CPU and memory cost are the same. The only overhead from multi-tenancy is one extra GORM query per request to set search_path (sub-millisecond on a warm connection).
Tenant noisy neighbours
Section titled “Tenant noisy neighbours”A single tenant cannot starve the cluster:
- Subscriber count is capped per-tenant from the super-admin console (default: 5,000).
- Connection pool has a per-tenant ceiling. A runaway tenant report query cannot drain the pool.
- Rate limiting (300 requests/minute) applies per tenant, not globally.
- RADIUS auth is shared but stateless; a flood from one tenant’s NAS doesn’t queue requests from another.
Failure modes
Section titled “Failure modes”| What fails | Blast radius |
|---|---|
| A tenant deletes all their data | That tenant only. No effect on the cluster. |
| A tenant exceeds their subscriber quota | That tenant is blocked from adding more subscribers until upgraded. |
| Postgres goes down | All tenants are offline. Run on managed Postgres or use streaming replication. |
| The host’s RADIUS container crashes | All tenants’ PPPoE auth pauses until it restarts (seconds). |
| nginx misroutes a subdomain | A 404 — but the request never reaches the API, so no data leak. |
Migrating between standalone and SaaS
Section titled “Migrating between standalone and SaaS”There is no automated converter. The two modes share a code base but differ in deployment topology, so migration is a deliberate operation:
- Standalone → SaaS: export the customer’s database, run the tenant onboarding flow to create an empty schema, then import the data into the new schema with a per-table
INSERT INTO tenant_xyz.<table> SELECT * FROM staging.<table>. Plan for downtime — the cleanest path is a maintenance window. - SaaS → standalone: export the single tenant’s schema with
pg_dump --schema=tenant_xyz, install ProxPanel standalone on the destination host, and restore. Cross-server backup format V2 (with embedded license key) helps when the standalone install will be on different hardware.
Both directions are real customer requests — they happen rarely but they happen. Contact info@proxrad.com before starting either one; we will walk through the export with you.
When SaaS mode is the wrong answer
Section titled “When SaaS mode is the wrong answer”Don’t deploy SaaS for:
- A single tenant. Use standalone — one less moving part.
- A tenant with regulatory data-residency that bans co-tenancy. Run them on their own host.
- A tenant whose subscriber count keeps growing. Migrate them out before they consume the cluster.
- A tenant that wants a custom build or custom feature. SaaS tenants run the cluster’s binary; you cannot deploy a custom build to one tenant without redeploying for all.
Operational checklist before going live
Section titled “Operational checklist before going live”A SaaS cluster is non-trivial to operate. Before opening signup to the public, confirm:
| Item | Why it matters |
|---|---|
Wildcard DNS record (*.saas.proxrad.com) configured and grey-clouded at Cloudflare | UDP services (RADIUS, WireGuard) cannot be proxied through Cloudflare; orange-cloud breaks PPPoE auth |
Wildcard TLS certificate issued (*.saas.proxrad.com) | Otherwise every new tenant subdomain shows a cert warning |
SAAS_TENANT_JWT_SECRET and SAAS_SUPERADMIN_JWT_SECRET set to independent values | Reusing the same secret across realms collapses the realm boundary |
SAAS_RELAY_SECRET set, license-server email relay tested | Welcome emails are silent-fail otherwise |
| Cloud-backup quota policy decided per plan | Trial tenants will fill 100 MB in days; you need a quota story before signup opens |
| Stripe webhooks configured (or your billing provider equivalent) | Plan upgrades from billing events drive feature gating |
Postgres max_connections sized for tenant count × per-tenant pool | A cluster sized for 10 tenants will choke at 30 |
Backup of the public schema scheduled | The tenant registry is irreplaceable; back it up separately from tenant schemas |
| Monitoring on host CPU/RAM/disk/Postgres connections | A SaaS cluster fails differently from a single-tenant install — alert thresholds should match |
| Super-admin TOTP enrolment for every operator | Stolen super-admin credentials are far worse than a stolen tenant admin |
A SaaS cluster that misses any of these can be live-but-fragile in ways that aren’t obvious until something breaks. Treat the checklist as a hard gate.
Related pages
Section titled “Related pages”- Tenant Onboarding — self-service signup, schema creation, first admin user.
- Schema-Per-Tenant Isolation — the
search_pathmechanism in detail and how it is enforced. - Wildcard Subdomain Routing — nginx regex, Cloudflare grey-cloud requirement.
- Tenant Backups — per-tenant cloud namespace, retention, encryption.
- Super Admin Console — managing tenants, billing, system health across the cluster.