Skip to content

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.

PathWho triggers itWhen to use it
Self-service signupThe prospect, from saas.proxrad.com/signupFree 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 creationYou, from the super-admin consoleInbound 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.

  1. 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.

  2. Tenant row insert. A row is inserted into public.tenants with the slug, the chosen plan, an initial status of provisioning, and the schema name (tenant_<slug> after replacing dashes with underscores).

  3. Schema creation. CREATE SCHEMA tenant_<slug> runs in a transaction. If it fails, the tenant row is rolled back.

  4. 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.

  5. Index creation. All standard indexes are built — most importantly the partial unique index on subscribers(username) WHERE deleted_at IS NULL that prevents the soft-delete username collision documented in Subscribers.

  6. 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.

  7. First admin user. A user row is inserted into tenant_<slug>.users with the email and password from the signup form. The user gets user_type = 'admin' (no permission group — admins bypass the permission system).

  8. System preferences. A row is inserted into tenant_<slug>.system_preferences with the chosen timezone, currency, company name, and default theme. These are the values shown in Settings the first time the admin logs in.

  9. Tenant status update. tenants.status flips from provisioning to active. The tenantCache (in backend/internal/middleware/tenant.go) is invalidated so the new subdomain is recognised on the next request.

  10. 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).

The public signup form lives at https://saas.proxrad.com/signup. The prospect fills in:

FieldValidationStored as
Company / ISP name2–100 charstenants.company_name, also system_preferences.company_name
Subdomain^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$, not reserved, not takentenants.subdomain and tenants.schema_name
Admin emailValid email, not yet used by any tenantusers.email, users.username
Admin passwordAt least 10 chars, mixed case + digitbcrypt hash in users.password
TimezoneIANA zone (e.g. Asia/Beirut)system_preferences.system_timezone
CurrencyISO 4217 (e.g. USD)system_preferences.currency
Plantrial, starter, protenants.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.

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.

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 = true for 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.

Every newly-created tenant gets the same four permission groups, with the same default-permission membership:

GroupPurposeDefault permissions
SALESFront-line staff who add subscribers and renew accountssubscribers.view/create/edit/renew/refill_quota, services.view, prepaid.view/edit, dashboard.view
SUPPORTTechnical staff who troubleshoot but don’t billsubscribers.view/edit/disconnect/ping/view_graph, sessions.view, nas.view, dashboard.view
COLLECTORField collectors taking paymentssubscribers.view/refill_quota, transactions.view/create, invoices.view/create, dashboard.view
READ_ONLYAuditors and observersevery *.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.

A tenant can attach their own domain (panel.myisp.com) in addition to the default subdomain. The flow:

  1. Tenant adds panel.myisp.com in Settings → Branding → Custom Domain.
  2. They are shown a CNAME record to add at their DNS provider: panel.myisp.com → <slug>.saas.proxrad.com.
  3. Once the CNAME resolves, the tenant clicks Verify. The backend checks the CNAME and writes the domain into tenants.custom_domain.
  4. The backend issues a Let’s Encrypt certificate via certbot running inside the API container, scoped to that domain only.
  5. 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.

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 provisioning for more than 5 minutes are flagged as failed.
  • Their schema (if it exists) is dropped with DROP SCHEMA tenant_<slug> CASCADE.
  • The tenants row is moved to status = failed and the prospect is sent an email pointing them to support.

Common failure causes:

SymptomCauseFix
CREATE SCHEMA fails with permission deniedPostgres user lacks CREATE on the databaseGrant it: GRANT CREATE ON DATABASE proxpanel_saas TO proisp
Welcome email failsSaaS email relay misconfiguredCheck SAAS_RELAY_SECRET env var on the API; verify license-server /saas-email-relay is reachable
Slug rejected as duplicate but no tenant visibleSoft-deleted tenant with the same slugHard-delete from super-admin console, or pick a different slug
TLS not issued for custom domainCNAME hasn’t propagated, or the domain has CAA records that exclude Let’s EncryptWait 5 minutes; if persistent, ask the tenant to remove CAA

Once provisioning succeeds, the new admin lands on a guided first-run flow:

  1. Welcome screen introduces the panel and shows a 30-second video on adding the first NAS.
  2. Add NAS wizard: name, IP, RADIUS shared secret, MikroTik API credentials.
  3. Create first service plan: speed, FUP tier, price.
  4. Create first subscriber: username, password, service plan, region.
  5. 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.

A super-admin can suspend a tenant from the console — useful for non-payment, abuse investigations, or planned offboarding:

StateLogin behaviourAPI behaviourSubscriber impact
activeNormalNormalNone
suspendedReturns 403 with “this account has been suspended”All endpoints return 403Existing PPPoE sessions stay up; new auths still succeed (RADIUS uses the tenant’s data directly)
deletedReturns 404Returns 404RADIUS 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.

Both signup paths are governed by permissions on the super-admin side:

PermissionEffect
saas.tenants.viewSee the tenant list in the super-admin console.
saas.tenants.createUse the “New Tenant” form to provision tenants manually.
saas.tenants.editChange a tenant’s plan, quota, or status (suspend / reactivate).
saas.tenants.deletePermanently delete a tenant and its schema. Irreversible.
saas.signup.toggleDisable 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.