> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pensionsportal.ie/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenant Provisioning Runbook

> Step-by-step guide for onboarding a new broker tenant onto PensionsPortal.ie

## Overview

This runbook covers the manual process for provisioning a new broker tenant on PensionsPortal.ie. Each broker firm operates as an isolated tenant — they can only access their own employers, pension schemes, and member data.

Tenant provisioning is currently a **manual admin operation**. A self-service broker onboarding flow is planned but not yet implemented.

<Note>
  This runbook is intended for PensionsPortal.ie engineering staff and SuperAdmin operators. Do not share this document with broker firms.
</Note>

***

## 1. Tenant Model Overview

In PensionsPortal.ie, a "tenant" maps to one **broker firm**. The data hierarchy under a tenant is:

```
Broker (tenant)
  └── Employers (companies the broker serves)
        └── Pension Schemes
              └── Members
                    └── Contributions, KFH records, compliance documents
```

Tenant isolation is enforced at the service layer via `ActorContext` — every query is filtered by `brokerId`. A broker user cannot read, write, or enumerate any data belonging to a different broker. See the [Multi-Tenancy Architecture](/tenancy) for the full isolation model.

***

## 2. Pre-Provisioning Checklist

Complete all items before beginning the provisioning steps:

* [ ] Data Processing Agreement (DPA) signed with the broker firm
* [ ] Broker firm legal name and Companies Registration Office (CRO) number confirmed
* [ ] Primary contact email address confirmed (this becomes the BrokerAdmin login)
* [ ] Primary contact full name confirmed
* [ ] Agreed billing plan confirmed (and payment method set up if applicable)
* [ ] Subdomain or white-label domain requirements noted (future — not yet implemented)
* [ ] Broker firm has been briefed on GDPR responsibilities as a data controller

<Warning>
  Do not provision a tenant without a signed DPA. PensionsPortal.ie processes personal data on behalf of the broker. Processing without a DPA in place violates GDPR Art. 28.
</Warning>

***

## 3. Provisioning Steps

### Step 1 — Connect to the production database

```bash theme={null}
# Option A: psql direct connection (requires DATABASE_URL_UNPOOLED from Vercel env)
psql $DATABASE_URL_UNPOOLED

# Option B: Drizzle Studio (browser UI — easier for non-SQL workflows)
npm run db:studio
# Opens at https://local.drizzle.studio
```

### Step 2 — Generate a broker ID

The broker is identified by a `brokerId` UUID that is stored on all associated users, employers, schemes, and members.

```sql theme={null}
-- Generate a new UUID to use as the brokerId
SELECT gen_random_uuid();
-- Example output: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
-- Copy this value — you will use it in Steps 3 and 6
```

<Note>
  Record the generated `brokerId` in the provisioning log for this broker. You will need it throughout this procedure and for future support requests. A dedicated broker admin UI at `/admin/brokers` is a planned roadmap item. Until implemented, the brokerId is the primary identifier for all support queries.
</Note>

There is currently no separate `brokers` table — the `brokerId` is a bare UUID propagated across the `users`, `employers`, `schemes`, and related tables.

### Step 3 — Create the BrokerAdmin user account

**Generate a secure temporary password:**

```bash theme={null}
# Generate a random temporary password
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# Output: a 32-character hex string — use this as the temporary password

# Generate a bcrypt hash of the temporary password (work factor 12)
node -e "const bcrypt = require('bcryptjs'); bcrypt.hash('PASTE_TEMP_PASSWORD_HERE', 12).then(h => console.log(h))"
```

**Insert the user record:**

```sql theme={null}
INSERT INTO users (
  id,
  email,
  password_hash,
  role,
  broker_id,
  full_name,
  created_at,
  updated_at
) VALUES (
  gen_random_uuid(),
  'admin@brokerfirm.ie',            -- replace with actual broker contact email
  '$2b$12$...paste_hash_here...',   -- bcrypt hash from command above
  'BrokerAdmin',
  'a1b2c3d4-e5f6-7890-abcd-ef1234567890',  -- replace with brokerId from Step 2
  'Firstname Lastname',             -- replace with actual full name
  NOW(),
  NOW()
);

-- Confirm the insert
SELECT id, email, role, broker_id, full_name, created_at
FROM users
WHERE email = 'admin@brokerfirm.ie';
```

<Warning>
  Never reuse temporary passwords across tenants. Generate a unique temporary password for each new broker provisioning. The broker must change this password on first login. **Note:** A forced first-login password change flow is a planned roadmap item. Until implemented, instruct the broker verbally or in the secure credential delivery message to change their password immediately.
</Warning>

### Step 4 — Communicate credentials securely

Send the following via a secure channel only. Do not send credentials over plain email.

**Acceptable channels:**

* 1Password Secure Share link (expires after first view)
* Bitwarden Send (single-use, time-limited)
* Encrypted email (if broker has PGP key on file)

**Message template:**

```
Your PensionsPortal.ie account has been created.

Login URL:          https://app.pensionsportal.ie
Email:              [broker contact email]
Temporary password: [temporary password]

IMPORTANT: Change your password immediately after first login.
Navigate to: Account Settings → Change Password

Getting started guide: https://docs.pensionsportal.ie/quickstart

If you have any issues logging in, contact: engineering@pensionsportal.ie
```

### Step 5 — Verify access

Log in to the portal as the new BrokerAdmin and confirm all of the following:

**Isolation checks — CRITICAL:**

* [ ] The BrokerAdmin can see zero employers initially (clean slate)
* [ ] The BrokerAdmin cannot navigate to or view any other broker's employers, schemes, or members
* [ ] Attempting to access another tenant's resource returns a 403 or empty result, not their data

**Functionality checks:**

* [ ] BrokerAdmin can create a new employer
* [ ] BrokerAdmin can create a pension scheme under that employer
* [ ] AI Compliance Assistant responds to queries
* [ ] Email notification received when a test member is enrolled (confirms Resend integration)
* [ ] Audit log records all test actions (check via SuperAdmin audit view or direct DB query)

```sql theme={null}
-- Verify audit log entries were created during verification
SELECT action, entity_type, timestamp
FROM audit_logs
WHERE actor_id = (SELECT id FROM users WHERE email = 'admin@brokerfirm.ie')
ORDER BY timestamp DESC
LIMIT 20;
```

### Step 6 — Log provisioning in the audit trail

```sql theme={null}
INSERT INTO audit_logs (
  actor_id,
  action,
  entity_type,
  entity_id,
  notes,
  timestamp
) VALUES (
  (SELECT id FROM users WHERE role = 'SuperAdmin' LIMIT 1),  -- use your SuperAdmin user ID
  'TENANT_PROVISIONED',
  'broker',
  'a1b2c3d4-e5f6-7890-abcd-ef1234567890',  -- brokerId from Step 2
  'New broker onboarded: [Broker Firm Legal Name] — DPA signed [date] — primary contact: [email]',
  NOW()
);
```

***

## 4. Failure and Rollback Plan

If the provisioning procedure fails or must be abandoned mid-way:

**Step 1 — Identify the last successful step** by checking what records exist in the database.

**Step 2 — Remove partially-created records:**

```sql theme={null}
-- Remove the partially-created user (within the last hour as a safety guard)
DELETE FROM users
WHERE broker_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
  AND created_at > NOW() - INTERVAL '1 hour';

-- Remove any employers created during testing (if Step 5 was partially completed)
DELETE FROM employers
WHERE broker_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
  AND created_at > NOW() - INTERVAL '1 hour';
```

**Step 3 — Log the rollback:**

```sql theme={null}
INSERT INTO audit_logs (
  actor_id,
  action,
  entity_type,
  entity_id,
  notes,
  timestamp
) VALUES (
  (SELECT id FROM users WHERE role = 'SuperAdmin' LIMIT 1),
  'TENANT_PROVISIONING_ROLLED_BACK',
  'broker',
  'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  'Provisioning failed at step [N] — rolled back. Reason: [describe failure]',
  NOW()
);
```

**Step 4 — Diagnose the failure**, then retry from Step 1 with a new `brokerId`.

***

## 5. Post-Provisioning Verification Checklist

Complete this checklist and retain a copy in the provisioning log:

| Check                                               | Result      | Notes |
| --------------------------------------------------- | ----------- | ----- |
| BrokerAdmin can log in successfully                 | Pass / Fail |       |
| BrokerAdmin cannot see other tenants' data          | Pass / Fail |       |
| BrokerAdmin can create an employer                  | Pass / Fail |       |
| BrokerAdmin can create a scheme under that employer | Pass / Fail |       |
| BrokerAdmin can enrol a member                      | Pass / Fail |       |
| AI Compliance Assistant responds to queries         | Pass / Fail |       |
| Email notifications working                         | Pass / Fail |       |
| Audit log records all test actions                  | Pass / Fail |       |
| Provisioning logged in audit trail                  | Pass / Fail |       |
| Temporary test data cleaned up                      | Pass / Fail |       |

If any check fails, do not hand the account over to the broker. Investigate, resolve, and re-run the full verification before proceeding.

***

## 6. Off-Boarding (Broker Departure)

When a broker firm ends their contract with PensionsPortal.ie, follow this procedure to fulfil GDPR data return and erasure obligations.

<Warning>
  Pension scheme compliance records (scheme documents, contribution histories, KFH filings) are subject to a **statutory 7-year retention period** under Irish pensions legislation. You must NOT delete scheme-level records — only anonymise the personal data of individual members. Confirm retention obligations with legal before proceeding.
</Warning>

**Step 1 — Export all broker data:**

```bash theme={null}
# Generate a full data export for the broker
# Export scheme and member data scoped to this brokerId
pg_dump $DATABASE_URL_UNPOOLED \
  --format=custom \
  --no-owner \
  --no-acl \
  --file="broker-export-$(date +%Y%m%d).dump"

# Planned: A SuperAdmin export endpoint at /api/admin/export/[brokerId] is a roadmap item.
# Until implemented, export manually via pg_dump as above, then filter by brokerId
# when delivering the export to the broker.
```

**Step 2 — Deliver export to broker via secure channel** (1Password Secure Share or encrypted email).

**Step 3 — Anonymise all member PII** following the GDPR Art. 17 procedure in [Backup & Restore Runbook](/runbooks/backup-restore#7-data-anonymisation-procedure-gdpr-art-17--right-to-erasure):

```sql theme={null}
-- Anonymise all members belonging to this broker's schemes
-- WARNING: Irreversible. Confirm legal hold status before executing.
UPDATE members SET
  first_name            = 'DELETED',
  last_name             = 'DELETED',
  email                 = NULL,
  phone                 = NULL,
  pps_number_encrypted  = NULL,
  date_of_birth         = NULL,
  updated_at            = NOW()
WHERE scheme_id IN (
  SELECT s.id FROM schemes s
  JOIN employers e ON e.id = s.employer_id
  WHERE e.broker_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
);
```

**Step 4 — Disable all broker users:**

```sql theme={null}
UPDATE users
SET role = 'Disabled', updated_at = NOW()
WHERE broker_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
```

**Step 5 — Retain scheme compliance records** (do not delete — statutory retention applies):

```sql theme={null}
-- Read-only verification that records are retained
SELECT COUNT(*) AS schemes_retained
FROM schemes s
JOIN employers e ON e.id = s.employer_id
WHERE e.broker_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
```

**Step 6 — Log off-boarding in audit trail:**

```sql theme={null}
INSERT INTO audit_logs (
  actor_id,
  action,
  entity_type,
  entity_id,
  notes,
  timestamp
) VALUES (
  (SELECT id FROM users WHERE role = 'SuperAdmin' LIMIT 1),
  'TENANT_OFFBOARDED',
  'broker',
  'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  'Broker offboarded: [Broker Firm Legal Name]. Data exported [date], PII anonymised, users disabled. Scheme records retained under 7-year statutory requirement.',
  NOW()
);
```

**Post-off-boarding checklist:**

* [ ] Data export delivered to broker and receipt confirmed
* [ ] All member PII anonymised (names, emails, phone numbers, PPS numbers, dates of birth)
* [ ] All broker user accounts set to Disabled role (cannot log in)
* [ ] Scheme compliance records confirmed retained (statutory 7-year hold)
* [ ] Off-boarding logged in audit trail
* [ ] DPO notified for GDPR Art. 30 records of processing update
* [ ] Billing subscription cancelled
