Skip to main content
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.
This page is the operator runbook. If you are building a self-service onboarding flow, see the Onboarding tutorial for the end-user journey.

Prerequisites

Before you provision a client:
  1. The database schema, seed data, and onboard_client.sql functions have been loaded:
    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

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

ParameterRequiredDefaultNotes
p_org_nameYesDisplay name of the organization (shown in the UI)
p_org_slugYesURL-safe identifier, globally unique. Rejected if already taken. Use lowercase alphanumeric + hyphens.
p_owner_emailYesEmail address of the workspace owner. If a user with this email already exists, their record is reused.
p_owner_nameNoLocal part of emailDisplay name for the owner
p_workspace_nameNoSame as p_org_nameWorkspace label; slug is derived automatically
p_planNofreeBilling plan: free, starter, growth, or enterprise
p_timezoneNoUTCIANA timezone (e.g. America/New_York). Used for display and report schedules.
p_seatsNo10Seat 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:
{
  "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

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:
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:
-- 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

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:
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:
#!/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:
SELECT offboard_client('acme', 'suspend');
This sets organizations.status = 'suspended'. API requests from suspended workspace tokens receive a 403 Forbidden. To reinstate:
UPDATE organizations SET status = 'active' WHERE slug = 'acme';

Purge (permanent, irreversible)

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, …)
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.

GDPR erasure of one person

To erase a single person’s data without removing the entire tenant, use the RTBF API:
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 for the full RTBF workflow.

Troubleshooting

ErrorCauseFix
duplicate key value violates unique constraint "organizations_slug_key"Slug already in useChoose a different slug
permission denied for table organizationsRole lacks INSERT on the tableConnect as the schema owner or grant permissions to your role
CAUSELOOP_MASTER_KEY is not setApplication started without the master keySet CAUSELOOP_MASTER_KEY in .env before starting
no organization with slug "..." (from offboard_client)Slug not foundVerify the slug matches exactly — slugs are case-sensitive
RLS policy violation on audit_logGUC not set before INSERTThe function calls set_config('app.current_workspace', ...) internally; ensure you are not overriding it externally