Tenant Onboarding
A tenant is the unit of isolation on a SaaS cluster: one ISP, one subdomain, one schema, one set of admin users. Tenants can be created two ways — by the prospect themselves through a public signup page, or by a super-admin from the cluster console. Both paths end at the same place: a working <slug>.saas.proxrad.com panel with a signed-in admin and an empty subscriber list.
The two onboarding paths
Section titled “The two onboarding paths”| Path | Who triggers it | When to use it |
|---|---|---|
| Self-service signup | The prospect, from saas.proxrad.com/signup | Free trials, low-touch acquisition, leads from the marketing site. The flow gates trials behind email verification and stores billing details before the schema is created. |
| Super-admin creation | You, from the super-admin console | Inbound sales, migrations from another platform, high-value tenants who want hand-holding, or any case where you want to skip the trial gate. |
Both paths exercise the same backend pipeline — only the trigger differs. The pipeline itself is idempotent: if it fails halfway, retrying with the same slug will pick up where it left off rather than creating duplicate state.
What happens during onboarding
Section titled “What happens during onboarding”-
Slug validation. The submitted slug is checked against
^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$, against a reserved-words list (admin,api,www,mail,signup,billing), and against existing tenants. Duplicates and reserved words are rejected immediately. -
Tenant row insert. A row is inserted into
public.tenantswith the slug, the chosen plan, an initial status ofprovisioning, and the schema name (tenant_<slug>after replacing dashes with underscores). -
Schema creation.
CREATE SCHEMA tenant_<slug>runs in a transaction. If it fails, the tenant row is rolled back. -
Table population. The full set of tenant tables (
subscribers,services,nas_devices,radacct,transactions,invoices,users,resellers,audit_logs,system_preferences, etc. — about 60 tables in total) is created inside the new schema using the same DDL the standalone installer uses, but qualified with the tenant’s schema name. -
Index creation. All standard indexes are built — most importantly the partial unique index on
subscribers(username) WHERE deleted_at IS NULLthat prevents the soft-delete username collision documented in Subscribers. -
Permission seed. The 220 default permissions and the four canonical permission groups (
SALES,SUPPORT,COLLECTOR,READ_ONLY) are inserted, identical to the seed used by the standalone installer. -
First admin user. A user row is inserted into
tenant_<slug>.userswith the email and password from the signup form. The user getsuser_type = 'admin'(no permission group — admins bypass the permission system). -
System preferences. A row is inserted into
tenant_<slug>.system_preferenceswith the chosen timezone, currency, company name, and default theme. These are the values shown in Settings the first time the admin logs in. -
Tenant status update.
tenants.statusflips fromprovisioningtoactive. ThetenantCache(inbackend/internal/middleware/tenant.go) is invalidated so the new subdomain is recognised on the next request. -
Welcome email is queued through the cluster’s SaaS email relay. The email contains the login URL (
https://<slug>.saas.proxrad.com/), the admin email, and a link to set the password if the signup form left it blank.
The entire pipeline takes 2–4 seconds on a warm cluster. The newly-created subdomain is reachable as soon as step 9 completes — there is no DNS propagation delay because *.saas.proxrad.com is a wildcard record that is grey-clouded at Cloudflare (see Wildcard Subdomain Routing).
Self-service signup walkthrough
Section titled “Self-service signup walkthrough”The public signup form lives at https://saas.proxrad.com/signup. The prospect fills in:
| Field | Validation | Stored as |
|---|---|---|
| Company / ISP name | 2–100 chars | tenants.company_name, also system_preferences.company_name |
| Subdomain | ^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$, not reserved, not taken | tenants.subdomain and tenants.schema_name |
| Admin email | Valid email, not yet used by any tenant | users.email, users.username |
| Admin password | At least 10 chars, mixed case + digit | bcrypt hash in users.password |
| Timezone | IANA zone (e.g. Asia/Beirut) | system_preferences.system_timezone |
| Currency | ISO 4217 (e.g. USD) | system_preferences.currency |
| Plan | trial, starter, pro | tenants.plan |
If the prospect picked a paid plan, the form proceeds to a billing-details step. Provisioning is gated until a valid card is on file — the tenant row exists with status = pending_payment but the schema is not created until payment succeeds.
After provisioning, the prospect is redirected to https://<slug>.saas.proxrad.com/ and signed in automatically by a short-lived single-use token in the redirect URL.
Super-admin-driven onboarding
Section titled “Super-admin-driven onboarding”From the super-admin console (https://saas.proxrad.com/admin/tenants), an authorised super-admin sees the full tenant list and a New Tenant button. The form mirrors the public signup but adds:
- Skip email verification — for tenants moved over from another platform whose email is already known to be theirs.
- Initial subscriber quota — overrides the plan default.
- Custom plan — assign a non-standard plan with arbitrary limits, for tenants on bespoke contracts.
- Send welcome email — uncheck if the tenant is being onboarded face-to-face and you don’t want the auto-email to fire.
The same pipeline runs. The tenant lands in active status immediately with the admin user pre-created.
Initial admin user
Section titled “Initial admin user”The first user inserted into the tenant’s users table has:
user_type = 'admin'— bypasses the permission system (admins have every permission implicitly).permission_group_id = NULL— admins are not assigned to a permission group; the auth middleware short-circuits the permission check for them.is_active = true.email_verified = truefor super-admin-created tenants,false(pending verification) for self-service.- A bcrypt-hashed password.
The admin can then create resellers, assign permission groups to them, and invite additional admin users from Settings → Users.
Permission seed
Section titled “Permission seed”Every newly-created tenant gets the same four permission groups, with the same default-permission membership:
| Group | Purpose | Default permissions |
|---|---|---|
| SALES | Front-line staff who add subscribers and renew accounts | subscribers.view/create/edit/renew/refill_quota, services.view, prepaid.view/edit, dashboard.view |
| SUPPORT | Technical staff who troubleshoot but don’t bill | subscribers.view/edit/disconnect/ping/view_graph, sessions.view, nas.view, dashboard.view |
| COLLECTOR | Field collectors taking payments | subscribers.view/refill_quota, transactions.view/create, invoices.view/create, dashboard.view |
| READ_ONLY | Auditors and observers | every *.view permission, nothing else |
These are starting points — every tenant can edit the groups, create new ones, or assign individual permissions à la carte from Settings → Permissions.
Custom domains
Section titled “Custom domains”A tenant can attach their own domain (panel.myisp.com) in addition to the default subdomain. The flow:
- Tenant adds
panel.myisp.comin Settings → Branding → Custom Domain. - They are shown a CNAME record to add at their DNS provider:
panel.myisp.com → <slug>.saas.proxrad.com. - Once the CNAME resolves, the tenant clicks Verify. The backend checks the CNAME and writes the domain into
tenants.custom_domain. - The backend issues a Let’s Encrypt certificate via
certbotrunning inside the API container, scoped to that domain only. - nginx is told to terminate TLS for the new domain. The first request to
https://panel.myisp.com/resolves to the same tenant as<slug>.saas.proxrad.com/.
Custom-domain provisioning takes about 30–60 seconds end-to-end. Renewal is automatic; the API container runs certbot renew daily at 03:30 in the tenant’s timezone.
Failure recovery
Section titled “Failure recovery”If the onboarding pipeline fails partway through, the tenant row stays in provisioning status and the orphan schema (if any) is dropped on the next cleanup pass. The cleanup runs every 10 minutes:
- Tenants in
provisioningfor more than 5 minutes are flagged as failed. - Their schema (if it exists) is dropped with
DROP SCHEMA tenant_<slug> CASCADE. - The
tenantsrow is moved tostatus = failedand the prospect is sent an email pointing them to support.
Common failure causes:
| Symptom | Cause | Fix |
|---|---|---|
CREATE SCHEMA fails with permission denied | Postgres user lacks CREATE on the database | Grant it: GRANT CREATE ON DATABASE proxpanel_saas TO proisp |
| Welcome email fails | SaaS email relay misconfigured | Check SAAS_RELAY_SECRET env var on the API; verify license-server /saas-email-relay is reachable |
| Slug rejected as duplicate but no tenant visible | Soft-deleted tenant with the same slug | Hard-delete from super-admin console, or pick a different slug |
| TLS not issued for custom domain | CNAME hasn’t propagated, or the domain has CAA records that exclude Let’s Encrypt | Wait 5 minutes; if persistent, ask the tenant to remove CAA |
Welcome experience
Section titled “Welcome experience”Once provisioning succeeds, the new admin lands on a guided first-run flow:
- Welcome screen introduces the panel and shows a 30-second video on adding the first NAS.
- Add NAS wizard: name, IP, RADIUS shared secret, MikroTik API credentials.
- Create first service plan: speed, FUP tier, price.
- Create first subscriber: username, password, service plan, region.
- Test PPPoE connection — the panel pings the NAS to confirm RADIUS is reachable and walks the admin through a real PPPoE test on the customer device.
The wizard is dismissible at any step. Tenants who already know the platform can skip straight to Subscribers and start adding accounts in bulk.
Suspending and reactivating tenants
Section titled “Suspending and reactivating tenants”A super-admin can suspend a tenant from the console — useful for non-payment, abuse investigations, or planned offboarding:
| State | Login behaviour | API behaviour | Subscriber impact |
|---|---|---|---|
| active | Normal | Normal | None |
| suspended | Returns 403 with “this account has been suspended” | All endpoints return 403 | Existing PPPoE sessions stay up; new auths still succeed (RADIUS uses the tenant’s data directly) |
| deleted | Returns 404 | Returns 404 | RADIUS rejects new auths after the next config reload (~30 seconds) |
The suspended-but-not-deleted state is deliberate: it lets you pause billing and panel access without immediately disrupting end-users. Operators commonly suspend tenants in a payment-dunning workflow and reactivate them once payment clears.
A reactivated tenant returns to full function with no data loss — the schema is untouched while suspended.
Permissions
Section titled “Permissions”Both signup paths are governed by permissions on the super-admin side:
| Permission | Effect |
|---|---|
saas.tenants.view | See the tenant list in the super-admin console. |
saas.tenants.create | Use the “New Tenant” form to provision tenants manually. |
saas.tenants.edit | Change a tenant’s plan, quota, or status (suspend / reactivate). |
saas.tenants.delete | Permanently delete a tenant and its schema. Irreversible. |
saas.signup.toggle | Disable or re-enable public self-service signup. |
Tenant admins have no special permissions for onboarding — they don’t onboard other tenants. Inside their own tenant, they manage their own admin users and resellers as documented in Users & Permissions.
Related pages
Section titled “Related pages”- SaaS Overview — the multi-tenant model in context.
- Schema-Per-Tenant Isolation — what
tenant_<slug>schema actually means at the database level. - Wildcard Subdomain Routing — how
<slug>.saas.proxrad.comreaches the right tenant. - Super Admin Console — managing tenants after they’re onboarded.
- Users & Permissions — the admin / reseller model inside each tenant.