---
title: Operating the registration module
path: tutorials/operating-the-registration-module
status: published
---

# Operating the registration module

You're the operator. You want to open this ScaiControl deployment to new customers, either as a public sign-up, by invitation only, or via a waitlist with manual approval. This tutorial walks through each path, plus the day-to-day operations (issuing, revoking, switching modes) you'll do once you're live.

Prerequisites:
- A `super_admin` account on this deployment.
- The browser open at `/admin/registration`.
- A rough idea of which partner the new tenants should attach to (the **default partner ID** — set once in the Settings tab).

If you're new to the model first, read [Registration modes](../concepts/registration-modes).

## 1. First-time setup

The Settings tab is the only screen that matters for setup. Open **Admin → Registration → Settings**:

1. **Mode** — start with `disabled` (the safe default). You'll flip this in step 4.
2. **Default partner ID** — paste the UUID of the partner that new self-serve / waitlist sign-ups should attach to. (Invitations can override this per-invitation.)
3. **Default trial pack slug** — optional, but recommended. If you have a "trial-bundle" service pack, set its slug here and every new tenant is auto-subscribed on creation.
4. **Email-domain allow / block lists** — optional. Set `allowed_email_domains` if you want to lock the deployment to a specific organisation (e.g. `acme.com`).
5. **Save changes.**

You're set up — the deployment is in `disabled` mode and refuses every public registration path. Time to pick a real mode.

## 2. Enable invitation mode (closed beta)

The most common starting point. You manually decide who gets in.

In **Settings**, change `mode` to `invitation` and save. The Settings tab will ask you to confirm: "The public sign-up / waitlist form will stop accepting new requests; only admin-issued invitations will create new tenants." Click **Switch mode**.

Now issue your first invitation:

1. Open the **Invitations** tab.
2. Click **+ Issue Invitation**.
3. Fill in:
   - **Email**: `alice@acme.com`
   - **Partner ID**: pre-fills from your default; override if this tenant attaches to a different partner.
   - **Company name** (optional, for display).
   - **TTL hours** (optional, defaults to 336 = 14 days).
4. Click **Issue & email**.

The modal closes and a green banner appears:

> Invitation issued — copy the accept URL if needed
> https://scaicontrol.scailabs.ai/auth/accept-invitation?token=...

In v1, **you paste this URL into your own email client** (whatever you use to talk to prospects). The token is the only secret part — once the recipient clicks it, ScaiControl handles the rest. The raw token is shown to you once and is never stored unhashed.

When the recipient accepts:
- Their tenant + `tenant_admin` user are created.
- The invitation row flips to `accepted`.
- ScaiKey emails them a "Welcome — set your password" link.
- `registration.completed.v1` fires (your CRM picks it up).

## 3. Enable waitlist mode (capacity-constrained launches)

You expect more interest than you can handle. Open the gates partially: the public form collects everyone, you decide who comes in.

In **Settings**:
- Change `mode` to `waitlist`.
- Configure **waitlist capture fields** — defaults to `["company_name", "use_case"]`; add anything else you want the form to ask for. The public form renders an input per slug.
- Save.

The public form is now live at `<portal>/auth/join-waitlist`. Visitors fill in their email + your configured extras and queue up.

To process the queue:

1. Open the **Waitlist** tab. Pending entries are sorted oldest-first.
2. For each entry you want in:
   - Click **Approve**.
   - Optionally override the trial pack or the invitation TTL.
   - Click **Approve & issue invitation**.
3. A green banner appears with the accept URL — paste it into your email client to the prospect.

The entry's status flips to `approved`, the invitation appears in the **Invitations** tab, and the recipient lands on the same accept page as a directly-issued invitation. From their perspective it's identical.

To reject:
- Click **Reject**, optionally include a reason (logged for your records, *not* emailed to the user).

Duplicates are auto-detected — if someone tries to join the waitlist with an email that's already a user or has a pending invitation, the entry lands with status `duplicate` and you'll see a ghost badge. No event is emitted for duplicates; admin churn stays low.

## 4. Enable self-serve mode (public launch)

You're ready for the world. Anyone with an email passing your domain policy can sign up themselves.

In **Settings**:
- Change `mode` to `self_serve`.
- **Crucially**: re-check your **email-domain allow / block lists**. Without them, anyone with any email can create a tenant.
- Save and confirm.

The Settings tab will warn you: "Self-serve sign-up will be open to the public. Make sure the email-domain allow/block lists are set the way you want them."

The portal login page now shows a **NEW HERE? — Create a tenant** CTA. The public sign-up form lives at `<portal>/auth/sign-up`.

## 5. Day-to-day operations

### Resend an invitation

If a prospect lost the email, open **Invitations**, find their row, and click **Resend**. Same token, no state change — useful when their inbox ate the first one.

If you want the *old* link to stop working too (e.g. you suspect it was forwarded), click **Rotate** instead. A fresh raw token is minted, the old token's hash is overwritten, and the previous link returns "invitation invalid" on click.

### Revoke an invitation

For a prospect you've decided not to onboard after all:

- Open **Invitations**, find the row, click **Revoke**.
- The link stops working immediately.
- `invitation.revoked.v1` fires.

You can only revoke `pending` invitations. Already-accepted invitations live on as the resulting tenant — disable the tenant from **Admin → Tenants** if you need to undo onboarding.

### Switch modes safely

Every mode switch shows you a preview of what's being stranded:

> Switching to `invitation` will leave 17 pending waitlist entries unaddressed.
> The public sign-up / waitlist form will stop accepting new requests; only admin-issued invitations will create new tenants.

Click **Cancel** to back out, or **Switch mode** to proceed. Pending records aren't touched by the switch — they stay in their queue, you can still approve them manually, you just stop receiving new ones until you switch back.

A typical operator arc:

```
launch day → invitation (manual, controlled)
   ↓ (after a week of soak)
soft-launch → waitlist (collect demand)
   ↓ (capacity confirmed)
GA → self_serve (open the gates)
```

You can move back at any time. Moving from `self_serve` → `disabled` is the emergency-stop pattern: existing users keep logging in, but no new tenants can register.

## 6. Watching the work queue

The **Dashboard** (for super_admins) shows a Registration widget at the top:

```
Registration                              [ invitation ]
Pending invitations: 3 (oldest 2d)
Waitlist (pending):  0
Last 7d activity:    1 accepted · 0 approved
```

If a number looks wrong — invitations stacking up because nobody's accepting, waitlist growing faster than you're approving — it's the cue to dig in. Click **Open registration admin →** to jump straight to the tabs.

## 7. Common pitfalls

| Symptom | Likely cause | Fix |
|---|---|---|
| "Self-serve sign-up is not currently enabled" on the public form | Mode isn't `self_serve` | Flip in Settings, or rely on the portal's mode-aware CTA. |
| "No partner_id available" error on register | `default_partner_id` is empty in settings | Set it in the Settings tab. |
| Welcome email never arrives, but tenant exists | ScaiKey leg failed (`password_email_sent: false`) | Open the user in **Admin → Users**, trigger ScaiKey password-reset manually. |
| Invitation token returns "expired" too quickly | TTL is too short | Raise `invitation_ttl_hours` in Settings (default 336 = 14 days). |
| Pending invitations stuck after `disabled` switch | They stay valid — by design | Either accept them, revoke them one by one, or wait for the hourly expiry cron. |
| Email domain you trust gets rejected | Block list overrides allow list | Check both lists in Settings. |

## Next

- [Registration modes](../concepts/registration-modes) — the underlying model.
- [Webhooks](./webhooks) — wire the registration + invitation + waitlist topics to ScaiCRM.
- [Service packs](../concepts/service-packs) — what `default_trial_pack_slug` actually activates.
