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

# Client Provisioning

> Operator runbook for provisioning a new client tenant via onboard_client() — prerequisites, parameters, verification, and offboarding.

Every Causeloop client (customer) maps to one **organization** that owns one **workspace**. All provisioning happens inside a single database transaction via the `onboard_client()` PL/pgSQL function, which is idempotent with respect to duplicate slugs and emails.

<Note>
  This page is the operator runbook. If you are building a self-service onboarding flow, see the [Onboarding tutorial](/get-started/onboarding-tutorial) for the end-user journey.
</Note>

## Provision via the API (recommended)

`POST /v1/provisioning/clients` provisions a complete client in one call and returns the
initial API key (shown once) and an owner invite link. Authenticate with your platform
**provisioning key** (separate from tenant API keys; configure its SHA-256 hash in
`CAUSELOOP_PROVISION_KEY_HASHES`, generate with `python scripts/gen_provision_key.py`).

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/provisioning/clients \
  -H "Authorization: Bearer $CAUSELOOP_PROVISION_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "organization": {"name": "Acme Corp", "slug": "acme", "plan": "growth", "seats": 25, "timezone": "America/New_York"},
    "owner": {"email": "owner@acme.com", "name": "Jane Doe"}
  }'
```

Valid `plan` values: `free`, `starter`, `growth`, `enterprise`. The endpoint is idempotent:
re-calling with the same `slug` + owner returns the existing tenant (`created:false`, no
secret); reuse an `Idempotency-Key` to safely retry and recover the original response. The
underlying mechanism is the `onboard_client()` SQL function documented below.

See the [Provisioning API reference](/api-reference/provisioning) for the full request and
response schema.

## Prerequisites

Before you provision a client:

1. The database schema, seed data, and `onboard_client.sql` functions have been loaded:

   ```bash theme={null}
   psql "$DATABASE_URL" -f db/schema.sql
   psql "$DATABASE_URL" -f db/seed_reference.sql
   psql "$DATABASE_URL" -f db/onboard_client.sql
   ```

2. `DATABASE_URL` connects as a role with sufficient privileges. `onboard_client()` inserts into `organizations`, `workspaces`, `users`, `memberships`, and `audit_log`. The schema owner or a role with `INSERT` on those tables is required.

3. The org slug you plan to use is unique. The function rejects duplicate slugs with an error.

## Quick start — one command

```sql theme={null}
SELECT onboard_client(
    p_org_name    => 'Acme Corp',
    p_org_slug    => 'acme',
    p_owner_email => 'alice@acme.com'
);
```

This is all you need. Everything else defaults automatically.

## Parameters

| Parameter          | Required | Default              | Notes                                                                                                      |
| ------------------ | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `p_org_name`       | Yes      | —                    | Display name of the organization (shown in the UI)                                                         |
| `p_org_slug`       | Yes      | —                    | URL-safe identifier, **globally unique**. Rejected if already taken. Use lowercase alphanumeric + hyphens. |
| `p_owner_email`    | Yes      | —                    | Email address of the workspace owner. If a user with this email already exists, their record is reused.    |
| `p_owner_name`     | No       | Local part of email  | Display name for the owner                                                                                 |
| `p_workspace_name` | No       | Same as `p_org_name` | Workspace label; slug is derived automatically                                                             |
| `p_plan`           | No       | `free`               | Billing plan: `free`, `starter`, `growth`, or `enterprise`                                                 |
| `p_timezone`       | No       | `UTC`                | IANA timezone (e.g. `America/New_York`). Used for display and report schedules.                            |
| `p_seats`          | No       | `10`                 | Seat allotment for the organization                                                                        |

## What gets created

`onboard_client()` runs everything inside a single transaction. On success, it returns a JSON object with the new IDs:

```json theme={null}
{
  "org_id": "org_01j...",
  "workspace_id": "ws_01j...",
  "user_id": "usr_01j...",
  "membership_id": "mem_01j..."
}
```

Inside the transaction, the function:

1. Creates an `organizations` row with the given slug, name, plan, and seats
2. Creates a `workspaces` row linked to the organization
3. Creates (or finds) a `users` row for the owner email
4. Creates a `memberships` row linking the user to the workspace with the `admin` role
5. Creates a default `workspace_settings` row
6. Writes an `audit_log` entry (`workspace.created`) under the new workspace

## Step-by-step — with full options

```sql theme={null}
SELECT onboard_client(
    p_org_name       => 'Acme Corp',
    p_org_slug       => 'acme',
    p_owner_email    => 'alice@acme.com',
    p_owner_name     => 'Alice Smith',
    p_workspace_name => 'Acme Main',
    p_plan           => 'growth',
    p_timezone       => 'America/New_York',
    p_seats          => 50
);
```

Run this from `psql` or any Postgres client connected as the schema owner:

```bash theme={null}
psql "$DATABASE_URL" -c "SELECT onboard_client(
    p_org_name    => 'Acme Corp',
    p_org_slug    => 'acme',
    p_owner_email => 'alice@acme.com',
    p_plan        => 'growth',
    p_seats       => 50
);"
```

## Verification

After running `onboard_client()`, verify the tenant was created correctly:

```sql theme={null}
-- Check the organization
SELECT id, name, slug, plan, seats_total, status
FROM organizations
WHERE slug = 'acme';

-- Check the workspace
SELECT id, name, organization_id, status
FROM workspaces
WHERE organization_id = (SELECT id FROM organizations WHERE slug = 'acme');

-- Check the owner membership
SELECT u.email, m.role, m.workspace_id
FROM memberships m
JOIN users u ON u.id = m.user_id
WHERE m.workspace_id = (
    SELECT w.id FROM workspaces w
    JOIN organizations o ON o.id = w.organization_id
    WHERE o.slug = 'acme'
);

-- Check the audit log entry
SELECT action, actor_name, created_at
FROM audit_log
WHERE workspace_id = (
    SELECT w.id FROM workspaces w
    JOIN organizations o ON o.id = w.organization_id
    WHERE o.slug = 'acme'
)
ORDER BY created_at DESC
LIMIT 5;
```

You should see:

* One `organizations` row with `status = 'active'`
* One `workspaces` row with `status = 'active'`
* One `memberships` row with `role = 'admin'`
* One `audit_log` entry with `action = 'workspace.created'`

## Adding the first teammates

### Option A — invite by email

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/invitations \
  -H "Authorization: Bearer <owner-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"email": "bob@acme.com", "role": "analyst"}'
```

The invitation email is currently a stub in the development build. The invited user must accept via `POST /v1/invitations/{id}/accept`.

### Option B — add directly

If you already have the user's record:

```sql theme={null}
INSERT INTO memberships (id, workspace_id, user_id, role, created_at)
VALUES (
    'mem_' || gen_random_uuid(),
    '<workspace_id>',
    '<user_id>',
    'analyst',
    now()
);
```

## Bulk onboarding

To provision many clients at once, wrap multiple calls in a script:

```bash theme={null}
#!/usr/bin/env bash
# bulk_onboard.sh
while IFS=',' read -r name slug email plan; do
    psql "$DATABASE_URL" -c "SELECT onboard_client(
        p_org_name    => '$name',
        p_org_slug    => '$slug',
        p_owner_email => '$email',
        p_plan        => '$plan'
    );"
done < clients.csv
```

Where `clients.csv` has columns: `name,slug,email,plan`.

## Offboarding a client

### Suspend (reversible)

Suspending blocks all access to the workspace while retaining all data:

```sql theme={null}
SELECT offboard_client('acme', 'suspend');
```

This sets `organizations.status = 'suspended'`. API requests from suspended workspace tokens receive a `403 Forbidden`. To reinstate:

```sql theme={null}
UPDATE organizations SET status = 'active' WHERE slug = 'acme';
```

### Purge (permanent, irreversible)

```sql theme={null}
SELECT offboard_client('acme', 'purge');
```

This deletes the `organizations` row. The `ON DELETE CASCADE` constraint propagates the deletion through:

* `workspaces`
* All tenant-scoped tables beneath the workspace (issues, patterns, memberships, connectors, audit\_log, ...)

<Warning>
  `offboard_client('slug', 'purge')` is **irreversible**. All tenant data is permanently deleted. `users` rows are **not** deleted — users may belong to other organizations. Clean up user records separately if needed.

  Before purging, consider exporting the tenant's data: `POST /v1/gdpr/export-requests`.
</Warning>

### GDPR erasure of one person

To erase a single person's data without removing the entire tenant, use the RTBF API:

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/governance/rtbf/requests \
  -H "Authorization: Bearer <admin-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@acme.com", "mode": "erasure"}'
```

See [GDPR & data governance](/deploy-security/gdpr-governance) for the full RTBF workflow.

## Troubleshooting

| Error                                                                     | Cause                                      | Fix                                                                                                                   |
| ------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `duplicate key value violates unique constraint "organizations_slug_key"` | Slug already in use                        | Choose a different slug                                                                                               |
| `permission denied for table organizations`                               | Role lacks INSERT on the table             | Connect as the schema owner or grant permissions to your role                                                         |
| `CAUSELOOP_MASTER_KEY is not set`                                         | Application started without the master key | Set `CAUSELOOP_MASTER_KEY` in `.env` before starting                                                                  |
| `no organization with slug "..."` (from `offboard_client`)                | Slug not found                             | Verify the slug matches exactly — slugs are case-sensitive                                                            |
| RLS policy violation on `audit_log`                                       | GUC not set before INSERT                  | The function calls `set_config('app.current_workspace', ...)` internally; ensure you are not overriding it externally |
