# Tenant Onboarding Runbook

Four onboarding paths exist, each serving a different customer segment. This runbook covers the procedures, prerequisites, and troubleshooting for each.

> **Phase 1 note:** Only Path 1 (Sales-Led) is fully implemented. Paths 2-4 are documented for Phase 2+ and are included here for planning and design reference.

---

## Path 1: Sales-Led Onboarding (Enterprise)

For large customers where setup is handled by internal staff (engineering or customer success).

### Prerequisites

- Super admin access (level: `full` or `support`)
- Customer's plan ID, jurisdiction, and data residency region
- Customer admin's email address

### Procedure

```
1. CreateTenant RPC
   ├── name: "Acme Records"
   ├── slug: "acme-records" (URL-safe, unique)
   ├── planId: "<plan UUID>"
   ├── primaryJurisdiction: "US" (ISO 3166-1 alpha-2)
   └── dataResidency: "us-east-1"


   → Creates tenant with status=active
   → Seeds default roles, permissions, modules

2. InviteUser RPC
   ├── tenantId: "<new tenant ID>"
   ├── email: "admin@acme.com"
   └── roleSlug: "admin"


   → Creates Invitation with secure token
   → Checks seat quota (0 < plan.maxSeats → pass)
   → Sends invitation email

3. Customer admin clicks invitation link
   → GET /accept-invitation?token=xxx
   → ValidateInvitationToken RPC verifies token
   → Client shows acceptance UI (tenant name, role, consent)

4. AcceptInvitation RPC (single DB transaction)
   ├── 1. Mark invitation as accepted (atomic check-and-set)
   ├── 2. Validate tenant.status == active
   ├── 3. Validate IdP token → get Identity
   ├── 4. Find or create User record
   ├── 5. Create TenantUser (status=active)
   ├── 6. Assign admin role
   ├── 7. Set userJurisdiction
   └── 8. Record consent


   → COMMIT
   → Create session (outside transaction)
   → Audit log: "user.joined"

5. Admin invites their team
   → InviteUser / InviteUsersBulk RPCs
   → Each invited user follows the same accept flow
```

### Troubleshooting

| Issue                                       | Cause                                                | Fix                                       |
| ------------------------------------------- | ---------------------------------------------------- | ----------------------------------------- |
| CreateTenant returns AlreadyExists          | Slug is taken                                        | Choose a different slug                   |
| Invitation email not received               | Email service delay                                  | ResendInvitation RPC                      |
| AcceptInvitation returns FailedPrecondition | Token expired, already accepted, or tenant suspended | Check invitation status and tenant status |
| Seat quota exceeded                         | Plan seat limit reached                              | Upgrade plan or deactivate unused users   |

---

## Path 2: Self-Serve Signup (Phase 2+)

For customers who sign up directly from the website.

### Flow

```
1. Customer fills signup form
   ├── Tenant name, email, plan selection (Starter/Professional)
   ├── Jurisdiction (auto-detected, editable)
   ├── Promotion code (optional)
   └── Consent checkbox (ToS + DPA)

2. Authenticate with IdP (creates account if new)

3. SignupTenant RPC
   ├── Validate IdP token → Identity
   ├── Validate slug uniqueness
   ├── Validate plan is self-serve eligible
   ├── Create Tenant + seed defaults
   ├── Create TenantPlan
   ├── Create User + TenantUser (admin role)
   ├── Record consent
   ├── Redeem promotion code (if valid)
   └── Create session + audit log

4. Admin lands in onboarding wizard
   ├── "Invite your team" (InviteUsersBulk)
   ├── "Connect your data" (data source setup)
   └── "Set up your domain" (optional)
```

**Restrictions:** Only plans with `self_serve_eligible: true`.

---

## Path 3: SCIM Provisioning (Phase 2+)

For organizations managing user lifecycle through their IdP (Okta, Azure AD).

### Flow

```
Enterprise IdP
    │ SCIM 2.0 protocol
    │ POST /scim/v2/Users (create)
    │ PATCH /scim/v2/Users/:id (update)
    │ DELETE /scim/v2/Users/:id (deactivate)
    ▼
Platform SCIM endpoint
    │ Authenticated via tenant-specific SCIM bearer token
    ▼
ScimCreateUser RPC
    ├── Map SCIM attributes to User + TenantUser
    ├── Create TenantUser (status=active, bypasses invitation)
    ├── Assign role from SCIM attribute or tenant default
    ├── Seat quota check
    └── Consent: deferred to first login (pipeline step 5 blocks until recorded)
```

### SCIM Attribute Mapping

| SCIM Attribute                  | Maps To                                            |
| ------------------------------- | -------------------------------------------------- |
| `userName`                      | User.email                                         |
| `name.givenName` + `familyName` | User.name                                          |
| `active`                        | TenantUser.status (true=active, false=deactivated) |
| `urn:custom:role`               | Role slug → RoleId                                 |
| `urn:custom:department`         | Department slug → DepartmentId                     |

**Plan requirement:** Enterprise only (or Business + `sso-scim` add-on).

---

## Path 4: Domain-Based Auto-Join (Phase 2+)

For organizations allowing anyone with their email domain to self-register.

### Flow

```
User authenticates with IdP
    ▼
ResolveTenant RPC
    │ No TenantUser found
    │ But email domain matches TenantDomainAllowlist (autoJoin=true, verified)
    ▼
RequestAutoJoin RPC
    ├── Verify domain in allowlist with autoJoin=true
    ├── Verify domain is verified (verifiedAt != null)
    ├── Seat quota check
    ├── Create TenantUser (status=active, role=defaultRoleId)
    └── Collect consent
```

### Domain Verification Methods

1. **DNS TXT record** — add `coda-verify=<token>` to domain DNS
2. **Admin email** — verification sent to `admin@domain`

**Plan requirement:** Business and Enterprise plans only.

---

## Consent Requirements by Jurisdiction

| Jurisdiction     | Consent Model | Collected at Onboarding                                           |
| ---------------- | ------------- | ----------------------------------------------------------------- |
| US (CCPA)        | Opt-out       | ToS acceptance. Privacy notice with opt-out link.                 |
| Canada (PIPEDA)  | Opt-in        | ToS + explicit consent for data processing.                       |
| Mexico (LFPDPPP) | Opt-in        | ToS + Aviso de Privacidad. Explicit consent for sensitive data.   |
| Japan (APPI)     | Opt-in        | ToS + purpose-of-use statement. Explicit consent.                 |
| EU (GDPR)        | Opt-in        | ToS + DPA. Explicit consent with lawful basis. Right to withdraw. |

**SCIM exception:** Consent deferred to first login. Pipeline step 5 blocks access until recorded.

---

## Seat Quota and Invitations

Pending invitations count toward the seat quota:

```
effective_seat_count = active TenantUsers + pending Invitations (not expired, not revoked)
InviteUser checks: effective_seat_count < effective_max_seats
```

- Expired invitations free the seat automatically
- Re-inviting an existing pending email → error ("already invited") — use ResendInvitation instead
- Bulk invitations are atomic per-email (one failure doesn't block others)

---

## Onboarding Comparison

| Aspect               | Sales-Led         | Self-Serve         | SCIM                    | Auto-Join        |
| -------------------- | ----------------- | ------------------ | ----------------------- | ---------------- |
| **Who initiates**    | Super admin       | Customer           | IdP                     | User (automatic) |
| **Tenant creation**  | Super admin       | Customer           | Pre-existing            | Pre-existing     |
| **Default role**     | Set by inviter    | Admin (first user) | SCIM attr or default    | Domain allowlist |
| **Consent timing**   | At acceptance     | At signup          | Deferred to first login | At auto-join     |
| **Plan requirement** | Any               | Self-serve only    | Enterprise / SSO        | Business+        |
| **Seat quota**       | Checked at invite | Checked at invite  | Checked at provisioning | Checked at join  |
