# Self-hosted enterprise (/platform/enterprise/self-hosted)





On a self-hosted deployment with billing disabled, enable enterprise features through environment variables. Then create an organization and attach its workspaces: branding, PII redaction, permission groups, custom blocks, data drains, and organization audit scope depend on the organization that owns a workspace.

This guide covers the standard billing-disabled configuration. If billing is enabled, subscription gates apply and instance-organization variables are ignored.

## Enable the feature set [#enable-the-feature-set]

Set both the server flag and its browser twin, then restart the application:

```bash
ENTERPRISE_ENABLED=true
NEXT_PUBLIC_ENTERPRISE_ENABLED=true
```

The server flag controls access and the server-resolved workspace settings. The browser twin also enables controls that read browser configuration directly, including SSO on the login page and the Function block's **Sandbox** selector. On a Compose install or source checkout, `bunx sim-setup doctor` reports mismatched twins. See [Verify your install](/platform/self-hosting/verify).

Individual flags override the master switch. For example, to enable the suite with data drains disabled:

```bash
ENTERPRISE_ENABLED=true
NEXT_PUBLIC_ENTERPRISE_ENABLED=true
DATA_DRAINS_ENABLED=false
NEXT_PUBLIC_DATA_DRAINS_ENABLED=false
```

You can also leave the master switch unset and enable individual features. The [feature flag reference](#feature-flag-reference) lists them all.

## Choose an organization model [#choose-an-organization-model]

### One organization for the instance [#one-organization-for-the-instance]

Use this when everyone on the deployment belongs to the same company. Set the instance organization's name:

```bash
INSTANCE_ORG_NAME="Acme Inc"
```

New accounts join it at signup, and their workspaces are created as organization-owned. Optionally set its slug and an existing account to own it:

```bash
INSTANCE_ORG_SLUG=acme-inc
INSTANCE_ORG_OWNER_EMAIL=admin@acme.com
```

The organization is created on the first signup after enabling this mode. If the owner email is unset or does not match an existing account, the account that triggers creation becomes the owner. Transfer ownership later through the Admin API if needed. Creation is idempotent across replicas.

#### Backfill existing accounts and workspaces [#backfill-existing-accounts-and-workspaces]

Setting `INSTANCE_ORG_NAME` does not move existing accounts or workspaces. Run the consolidation script from the repository root. If the organization does not exist yet, provide an owner email that matches an **existing Sim account**:

```bash
# Preview the changes
DATABASE_URL=... \
INSTANCE_ORG_NAME="Acme Inc" \
INSTANCE_ORG_OWNER_EMAIL=admin@acme.com \
  bun run apps/sim/scripts/consolidate-users-into-organization.ts

# Apply the same changes
DATABASE_URL=... \
INSTANCE_ORG_NAME="Acme Inc" \
INSTANCE_ORG_OWNER_EMAIL=admin@acme.com \
  bun run apps/sim/scripts/consolidate-users-into-organization.ts --apply
```

The script adds accounts to the organization and attaches their workspaces. It is a dry run unless you pass `--apply`, and is safe to rerun. Accounts already in another organization are reported and skipped because an account can belong to only one organization. When the instance organization already exists, the script uses its current owner.

### Separate organizations managed through the Admin API [#separate-organizations-managed-through-the-admin-api]

Leave `INSTANCE_ORG_NAME` unset when one deployment serves multiple teams. Create organizations, add members, and attach workspaces through the Admin API.

Generate an admin key once:

```bash
openssl rand -hex 32
```

Persist that value as `ADMIN_API_KEY` in the **application's environment**, then restart the application. Use the same value in the shell or client issuing the following requests:

```bash
ADMIN_API_KEY='<the-same-key-configured-on-the-application>'
```

Generating or assigning a value in the caller's shell does not configure the running application. Requests use the exact `x-admin-key` header.

<Steps>
  <Step>
    ### Create an organization [#create-an-organization]

    The owner account must exist and must not belong to another organization.

    ```bash
    curl -X POST https://sim.example.com/api/v1/admin/organizations \
      -H "x-admin-key: $ADMIN_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name": "Acme Inc", "ownerId": "user_123", "slug": "acme-inc"}'
    ```

    Use the returned organization ID as `ORG_ID` in subsequent commands. Creating the organization does not attach a workspace.
  </Step>

  <Step>
    ### Add members [#add-members]

    ```bash
    curl -X POST https://sim.example.com/api/v1/admin/organizations/$ORG_ID/members \
      -H "x-admin-key: $ADMIN_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"userId": "user_456", "role": "member"}'
    ```
  </Step>

  <Step>
    ### Attach a workspace [#attach-a-workspace]

    Enterprise settings apply only to workspaces the organization owns. Generate a fresh UUID for each new move. From the repository root:

    ```bash
    OPERATION_ID=$(bun -e 'import { generateId } from "@sim/utils/id"; process.stdout.write(generateId())')

    curl -X POST https://sim.example.com/api/v1/admin/dashboard/workspaces/$WORKSPACE_ID/move \
      -H "x-admin-key: $ADMIN_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"operationId\": \"$OPERATION_ID\", \"destinationOrganizationId\": \"$ORG_ID\"}"
    ```

    Keep the same `OPERATION_ID` when retrying this move; generate a new one only for a new operation. Inspect `data.followUpJobs` in the response for pending or failed follow-up work. Check it again with:

    ```bash
    curl "https://sim.example.com/api/v1/admin/dashboard/workspaces/$WORKSPACE_ID/move-operations/$OPERATION_ID?destinationOrganizationId=$ORG_ID" \
      -H "x-admin-key: $ADMIN_API_KEY"
    ```
  </Step>

  <Step>
    ### Configure organization settings [#configure-organization-settings]

    Use the Settings UI or these API requests:

    <Tabs items="['Branding', 'Data retention', 'Session policy']">
      <Tab value="Branding">
        ```bash
        curl -X PATCH https://sim.example.com/api/v1/admin/organizations/$ORG_ID/whitelabel \
          -H "x-admin-key: $ADMIN_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"brandName": "Acme AI", "hidePoweredBySim": true}'
        ```
      </Tab>

      <Tab value="Data retention">
        ```bash
        curl -X PATCH https://sim.example.com/api/v1/admin/organizations/$ORG_ID/data-retention \
          -H "x-admin-key: $ADMIN_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"logRetentionHours": 2160}'
        ```
      </Tab>

      <Tab value="Session policy">
        ```bash
        curl -X PATCH https://sim.example.com/api/v1/admin/organizations/$ORG_ID/session-policy \
          -H "x-admin-key: $ADMIN_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{"maxSessionHours": 168, "idleTimeoutHours": 48}'
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

When invitations are disabled with `DISABLE_INVITATIONS` and `NEXT_PUBLIC_DISABLE_INVITATIONS`, use the Admin API to manage organization membership and workspace access. Internal members join the organization. External workspace members receive access to a specific workspace without joining the organization.

### Read audit logs and permission groups [#read-audit-logs-and-permission-groups]

Admin-key reads do not require an organization or Enterprise subscription:

```bash
# Deployment audit entries, with filters also available in the UI
curl "https://sim.example.com/api/v1/admin/audit-logs?limit=50&workspaceId=$WORKSPACE_ID" \
  -H "x-admin-key: $ADMIN_API_KEY"

# One audit entry
curl "https://sim.example.com/api/v1/admin/audit-logs/$AUDIT_LOG_ID" \
  -H "x-admin-key: $ADMIN_API_KEY"

# Permission groups, optionally scoped to an organization
curl "https://sim.example.com/api/v1/admin/access-control?organizationId=$ORG_ID" \
  -H "x-admin-key: $ADMIN_API_KEY"
```

The organization-scoped `/api/v1/audit-logs` endpoint uses an API key whose owner is an organization admin or owner. Use the admin endpoint when no organization exists yet. See [Audit logs](/platform/enterprise/audit-logs).

### Delete an organization [#delete-an-organization]

Deleting an organization removes its memberships, invitations, permission groups, and organization settings. Its workspaces are detached and survive. A non-terminal subscription prevents deletion; cancel it first.

Confirm the target by passing its exact slug:

```bash
curl -X DELETE "https://sim.example.com/api/v1/admin/organizations/$ORG_ID?confirmSlug=acme-inc" \
  -H "x-admin-key: $ADMIN_API_KEY"
```

## Schedule the background jobs [#schedule-the-background-jobs]

Configure a scheduler to call the maintenance endpoints. Generate a cron secret:

```bash
openssl rand -hex 32
```

Persist the same value as `CRON_SECRET` on the application and its scheduler. Each endpoint requires `Authorization: Bearer <CRON_SECRET>` and returns `401` if the secret is unset or does not match.

| Feature | Endpoint | Suggested schedule | Included in Helm and production Compose schedules |
| - | - | - | - |
| Data drains | `GET /api/cron/run-data-drains` | Hourly | Yes |
| SCIM reconciliation | `GET /api/cron/scim-reconcile` | Hourly | Yes |
| Retention: logs | `GET /api/logs/cleanup` | Daily | No |
| Retention: soft deletes | `GET /api/cron/cleanup-soft-deletes` | Daily | No |
| Retention: Chat tasks | `GET /api/cron/cleanup-tasks` | Daily | No |
| Retention: file versions | `GET /api/cron/cleanup-file-versions` | Daily | No |
| OAuth token cleanup | `GET /api/cron/cleanup-oauth-tokens` | Hourly | Yes |

Included schedules still require the cron service to be configured and running. For an external scheduler, a request looks like:

```bash
curl -H "Authorization: Bearer $CRON_SECRET" \
  https://sim.example.com/api/logs/cleanup
```

<Callout type="warn">
  Configuring retention windows and enabling deletion are separate actions. Set `DATA_RETENTION_ENABLED=true` to allow retention cleanup, and schedule the four retention endpoints yourself, for example through Helm's `cronjobs.jobs`. Neither the flag alone nor the shipped cron schedules expire retained product data. Billing-disabled deployments use only explicitly configured windows, without hosted-plan defaults.
</Callout>

Retention and data-drain work use the configured job backend. Without Trigger.dev, they have in-process execution paths; with Trigger.dev configured, external workers run the jobs. SCIM reconciliation runs directly through its authenticated cron endpoint and does not require Trigger.dev. See [Background jobs](/platform/self-hosting/background-jobs) for operational setup and [Data retention](/platform/enterprise/data-retention) for effective policy.

OAuth token cleanup removes expired and revoked credentials independently of sign-in activity. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim) for provider configuration.

## Feature flag reference [#feature-flag-reference]

Explicit individual flags override the master switch, including when set to `false`. With billing disabled and no enterprise flags set, custom branding, session policies, and the Sim Mailer inbox remain enabled by default. Other features require opt-in.

| Feature | Server variable | Browser variable |
| - | - | - |
| Enterprise suite | `ENTERPRISE_ENABLED` | `NEXT_PUBLIC_ENTERPRISE_ENABLED` |
| Organizations | `ORGANIZATIONS_ENABLED` | `NEXT_PUBLIC_ORGANIZATIONS_ENABLED` |
| Permission groups | `ACCESS_CONTROL_ENABLED` | `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` |
| SAML and OIDC sign-in | `SSO_ENABLED` | `NEXT_PUBLIC_SSO_ENABLED` |
| Directory provisioning | `SCIM_ENABLED` | `NEXT_PUBLIC_SCIM_ENABLED` |
| Custom branding — enabled by default | `WHITELABELING_ENABLED` | `NEXT_PUBLIC_WHITELABELING_ENABLED` |
| Audit logs | `AUDIT_LOGS_ENABLED` | `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` |
| Insights | `USAGE_MONITORING_ENABLED` | `NEXT_PUBLIC_USAGE_MONITORING_ENABLED` |
| Custom blocks | `CUSTOM_BLOCKS_ENABLED` | `NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED` |
| Session policies — enabled by default | `SESSION_POLICIES_ENABLED` | `NEXT_PUBLIC_SESSION_POLICIES_ENABLED` |
| Retention deletion | `DATA_RETENTION_ENABLED` | `NEXT_PUBLIC_DATA_RETENTION_ENABLED` |
| Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` |
| Credential groups | `CREDENTIAL_GROUPS` | — |
| Workspace forks | `FORKING_ENABLED` | `NEXT_PUBLIC_FORKING_ENABLED` |
| Sim Mailer inbox — enabled by default | `INBOX_ENABLED` | `NEXT_PUBLIC_INBOX_ENABLED` |
| Sandboxes | `SANDBOXES_ENABLED` | `NEXT_PUBLIC_SANDBOXES_ENABLED` |

### Sandbox readiness [#sandbox-readiness]

A Sandbox entitlement also requires a remote execution provider with credentials and an immutable Function base image. Until the server verifies readiness, the Function block offers no **Shell** language, and **Settings → Sandboxes** shows an upgrade notice instead of a sandbox list.

`NEXT_PUBLIC_SANDBOXES_ENABLED` exposes the Function block's **Sandbox** selector; it asserts readiness rather than checking it. Set it only after configuring the selected provider and Function base. `bunx sim-setup doctor` compares it with provider readiness.

JavaScript without `import`, `require`, a selected sandbox, or a sandbox file-path reference runs in local Node child workers in the app container when Sandbox flags are off. Python, Shell, JavaScript with external imports, and selected custom Sandboxes require the remote Function base and fail with a configuration error until it is ready. See [Sandboxes](/platform/self-hosting/sandboxes).

## Verify the setup [#verify-the-setup]

| Symptom | Check |
| - | - |
| A settings section is missing | Check its server flag and restart the app after changing environment variables. These values load at boot; a rebuild is not needed. |
| The login page has no SSO option, or Function has no Sandbox selector | Set the matching `NEXT_PUBLIC_` flag. Run `bunx sim-setup doctor` to check twins and Sandbox readiness. |
| **Settings → Sandboxes** shows an upgrade notice | Check Sandbox entitlement, provider credentials, and the immutable Function base. |
| An enabled feature has no effect in a workspace | Confirm that the workspace belongs to an organization. Use the backfill or workspace move procedure if needed. |
| Retention deletes nothing | Check `DATA_RETENTION_ENABLED`, explicit windows, scheduler requests, and `CRON_SECRET`. |

To inspect workspace ownership:

```sql
SELECT id, name, workspace_mode, organization_id FROM workspace;
```

A workspace with `personal` mode or a null `organization_id` does not inherit organization branding, PII redaction, permission groups, or drains.

## Related docs [#related-docs]

* [Environment variables](/platform/self-hosting/environment-variables)
* [Single sign-on](/platform/enterprise/sso)
* [Permission groups](/platform/enterprise/access-control)
* [Roles and permissions](/platform/permissions)
* [Workspaces](/platform/workspaces)
