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:
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#
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#
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_domainsto a list to lock the deployment to specific organisations (e.g.["acme.com"]for an Acme-internal tenant pool). - Set
blocked_email_domainsto 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:
- ScaiControl creates the tenant +
Userrow (statuspending). - ScaiControl calls ScaiKey
/admin/usersto pre-create the credential record. - ScaiControl calls ScaiKey
/password/forgot?purpose=welcometo email the recipient the "Welcome — set your password" link. - Recipient clicks the link, sets a password on ScaiKey.
- First successful login flips
User.statusfrompendingtoactive.
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#
- Operating the registration module — operator cookbook: enable each mode, issue and revoke, approve a waitlist entry, switch modes safely.
- Authentication and RBAC — the ScaiKey handshake in detail.
- Webhooks — subscribing to the registration + invitation + waitlist event topics.