Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Registration modes

How do new customers become tenants on this deployment? ScaiControl gives the operator one knob — registration_settings.mode — with four positions. This page explains what each one does, how they relate to invitations and waitlists under the hood, and what to expect when you flip between them.

The four modes#

Mode Who can register? What's open to the public?
disabled Nobody. Existing users continue to log in. Nothing. Sign-up and waitlist endpoints refuse.
self_serve Anyone with an email passing the allow/block-list policy. POST /auth/sign-up + the portal sign-up form.
invitation Only recipients of an admin-issued invitation token. Nothing public except POST /auth/accept-invitation.
waitlist Anyone can request access; admin manually approves. The waitlist form. Approval auto-issues an invitation.

Switching modes is one HTTP call (PUT /admin/platform/registration-settings) or one click in the Settings tab. The Settings tab also asks for confirmation before mode changes that would leave existing records stranded (e.g. switching away from waitlist while pending entries are queued).

Mental model: invitations are the backbone#

Regardless of which mode is active, every new tenant ends up routed through the same register_tenant() service function — which creates the tenant ID, the first tenant_admin user (status pending), and emits registration.completed.v1. The modes differ only in how the request enters the system:

flowchart LR subgraph self_serve [Self-serve] S1[Public sign-up form] --> S2[POST /auth/sign-up] --> S3[register_tenant] end subgraph invitation [Invitation] I1[Admin issues invitation] --> I2[Email with token] I2 --> I3[Recipient clicks link] --> I4[POST /auth/accept-invitation] --> I5[register_tenant] end subgraph waitlist [Waitlist] W1[Public waitlist form] --> W2[Entry queued] W2 --> W3[Admin approves] W3 --> W4[Auto-issues invitation] --> W5[Recipient clicks link] --> W6[POST /auth/accept-invitation] --> W7[register_tenant] end S3 --> X[ScaiKey pre-create user] I5 --> X W7 --> X X --> Y[ScaiKey welcome+set-password email]

Note the waitlist → invitation arrow. Approving a waitlist entry issues an invitation under the hood — same code path, same email template, same accept URL. Less code to maintain; the recipient sees a consistent experience whether they were invited directly or pulled off the queue.

State machines#

Invitations#

stateDiagram-v2 [*] --> pending pending --> accepted: recipient completes flow pending --> expired: cron flips past expires_at pending --> revoked: admin revokes accepted --> [*] expired --> [*] revoked --> [*]

Terminal states are absorbing. The expiry cron runs hourly at :41; rows past expires_at move to expired and emit invitation.expired.v1.

Waitlist entries#

stateDiagram-v2 [*] --> pending pending --> approved: admin approves (issues invitation) pending --> rejected: admin rejects (optional reason) pending --> duplicate: email already known approved --> [*] rejected --> [*] duplicate --> [*]

duplicate is auto-assigned at join time when the email is already a User row or already has a pending Invitation. The public response shape is identical to a fresh pending — the submitter doesn't learn that their email is on file.

Cross-cutting policy: email-domain allow/block#

Both allowed_email_domains and blocked_email_domains are enforced at every entry point (sign-up, invitation issue, waitlist join). Blocked always wins over allowed: a domain in both lists is rejected.

  • Leave both empty to allow any email.
  • Set allowed_email_domains to a list to lock the deployment to specific organisations (e.g. ["acme.com"] for an Acme-internal tenant pool).
  • Set blocked_email_domains to shut down known abusive sources.

The policy is settings-driven so changing it doesn't require a deploy — the next sign-up reads the updated list.

Cross-cutting policy: idempotency#

register_tenant() is idempotent on email. If a user with the submitted email already exists, the function returns that user's tenant ID rather than creating a duplicate. This means:

  • Retried form submissions don't create duplicate tenants.
  • The same response shape is returned whether the email is new or known — useful for not leaking the deployment's user base via the public sign-up endpoint.

Events fired#

Operators wiring these to ScaiCRM should know which topics fire for which transition:

Mode action Events emitted
Self-serve sign-up registration.completed.v1
Admin issues invitation invitation.issued.v1
Recipient accepts invitation invitation.accepted.v1 + registration.completed.v1
Admin revokes invitation invitation.revoked.v1
Cron expires invitation invitation.expired.v1
Public waitlist join (new entry) waitlist.entry_created.v1
Public waitlist join (duplicate detected) (no event — duplicates are silent)
Admin approves waitlist entry waitlist.entry_approved.v1 + invitation.issued.v1
Recipient accepts (from waitlist approval) invitation.accepted.v1 + registration.completed.v1
Admin rejects waitlist entry waitlist.entry_rejected.v1

Every event carries enough context (tenant_id, partner_id, email, source) for a downstream CRM to create or update the corresponding lead/account without a callback into ScaiControl.

Settings the operator controls#

In the Settings tab under Admin → Registration:

Setting What it does
mode The headline switch.
default_partner_id New tenants attach to this partner unless an invitation overrides it.
default_trial_pack_slug Auto-activate this service pack at registration time.
default_role Role granted to the first user (defaults to tenant_admin).
require_email_verification Reserved for waitlist double-opt-in (off in v1; the ScaiKey password-set email already proves ownership for sign-up + invitation).
allowed_email_domains / blocked_email_domains Domain policy described above.
waitlist_capture_fields Which extra fields the public waitlist form asks for (company_name, use_case, …).
invitation_ttl_hours How long invitations stay valid (default 336 = 14 days).
signup_rate_limit_per_hour Per-IP cap on public endpoints.
welcome_email_template_name / invitation_email_template_name Names of templates from the billing template system (optional — falls back to a built-in default).

What about ScaiKey?#

ScaiControl owns the tenant + first user. ScaiKey owns the credentials. The handoff (option (c) — see Authentication) is:

  1. ScaiControl creates the tenant + User row (status pending).
  2. ScaiControl calls ScaiKey /admin/users to pre-create the credential record.
  3. ScaiControl calls ScaiKey /password/forgot?purpose=welcome to email the recipient the "Welcome — set your password" link.
  4. Recipient clicks the link, sets a password on ScaiKey.
  5. First successful login flips User.status from pending to active.

Failures in step 2 or 3 don't roll back step 1 — the tenant exists, the operator can retry the ScaiKey legs manually from the admin UI.

Next#

Updated 2026-06-29 00:34:15 View source (.md) rev 2