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

# GCP GKE + Entra ID + Wharf + Studio V2

> Complete deployment guide for CrewAI Platform on Google Kubernetes Engine with Microsoft Entra ID SSO, Wharf trace collection, and Studio V2.

## Overview

This guide walks through a complete production deployment of CrewAI Platform on GKE using:

* **Cloud SQL** for PostgreSQL (four databases including Wharf) via Cloud SQL Auth Proxy with IAM authentication
* **Google Cloud Storage** for object storage via Workload Identity
* **Artifact Registry** for crew container images
* **Microsoft Entra ID** for SSO authentication
* **Wharf** for OTLP trace collection (enabled by default)
* **Studio V2** configured post-install

This is a self-contained guide. All required Helm values are included — no cross-references to other guides are needed for a complete deployment.

<Note>
  This guide assumes:

  * A GKE cluster running Kubernetes 1.28+ with Workload Identity enabled
  * Gateway API enabled on the cluster
  * `gcloud` CLI, `kubectl`, and Helm 3.10+ installed
  * An active Microsoft Entra ID (Azure AD) tenant
  * Basic familiarity with GCP services (Cloud SQL, GCS, Artifact Registry)
</Note>

***

## Prerequisites Checklist

Complete each item before running `helm install`:

* [ ] GKE cluster with Workload Identity enabled
* [ ] Gateway API enabled (`gcloud container clusters update --gateway-api=standard`)
* [ ] GCP APIs enabled (see below)
* [ ] GCP Service Account created with required IAM roles
* [ ] Workload Identity binding created for platform and crews namespaces
* [ ] Cloud SQL instance created with IAM authentication enabled
* [ ] All four databases created: `crewai_plus_production`, `crewai_plus_cable_production`, `crewai_plus_oauth_production`, `wharf`
* [ ] SQL privileges granted to IAM user on all four databases
* [ ] GCS bucket created
* [ ] Artifact Registry repository created (mutable tags, ends in `/crewai-enterprise`)
* [ ] Entra ID App Registration complete with redirect URI configured
* [ ] Redirect URI `https://<YOUR_DOMAIN>/auth/entra_id/callback` registered in Azure **before** `helm install`
* [ ] `factory-admin` App Role created in Azure and assigned to admin users
* [ ] `values.yaml` assembled with all placeholders filled in

<Warning>
  The Entra ID redirect URI **must** be registered in Azure before running `helm install`. Authentication will fail at login if it is missing. The redirect URI format is exactly: `https://<YOUR_DOMAIN>/auth/entra_id/callback`
</Warning>

***

## Part 1: GCP Infrastructure Setup

### Enable Required APIs

```bash theme={null}
gcloud services enable \
  container.googleapis.com \
  sqladmin.googleapis.com \
  storage-api.googleapis.com \
  artifactregistry.googleapis.com \
  iam.googleapis.com \
  certificatemanager.googleapis.com
```

### Set Shell Variables

```bash theme={null}
export GCP_PROJECT_ID="<YOUR_GCP_PROJECT_ID>"
export GCP_REGION="<YOUR_REGION>"            # e.g. us-central1
export GSA_NAME="crewai-platform"            # GCP Service Account name
export GKE_NAMESPACE="crewai"               # Kubernetes namespace for the Helm release
export KSA_NAME="crewai-sa"                 # Chart default SA name when release name is 'crewai'
export GKE_CLUSTER="<YOUR_CLUSTER_NAME>"
export SQL_INSTANCE="crewai-db"
export AR_REPO="crewai"
export GCS_BUCKET="crewai-prod-storage"
```

<Warning>
  `KSA_NAME` must match the ServiceAccount the chart creates. The chart default is `{release-name}-sa`. This guide uses release name `crewai`, producing `crewai-sa`. If your release name differs, either update `KSA_NAME` to match, or pin `serviceAccount.name: crewai-sa` in your Helm values.
</Warning>

### Create GCP Service Account

```bash theme={null}
gcloud iam service-accounts create $GSA_NAME \
  --display-name="CrewAI Platform" \
  --project=$GCP_PROJECT_ID
```

### Grant IAM Roles

```bash theme={null}
# GCS — object storage read/write
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/storage.objectAdmin"

# GCS — signed URL generation via IAM signBlob (required for Workload Identity)
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/iam.serviceAccountTokenCreator"

# Artifact Registry — push/pull crew container images
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/artifactregistry.writer"

# Cloud SQL — connect via Auth Proxy
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/cloudsql.client"

# Cloud SQL — IAM database authentication
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/cloudsql.instanceUser"

# Node pool GCE SA — pull crew images from Artifact Registry
GCE_SA="$(gcloud projects describe $GCP_PROJECT_ID --format='value(projectNumber)')-compute@developer.gserviceaccount.com"
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --member="serviceAccount:${GCE_SA}" \
  --role="roles/artifactregistry.reader"
```

### Bind Workload Identity

```bash theme={null}
# Platform namespace — web, worker, buildkit daemon pods
gcloud iam service-accounts add-iam-policy-binding \
  ${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[${GKE_NAMESPACE}/${KSA_NAME}]"

# Crews namespace — build pods that push images to Artifact Registry
gcloud iam service-accounts add-iam-policy-binding \
  ${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[crewai-crews/default]"
```

***

## Part 2: Cloud SQL Setup

### Create the Cloud SQL Instance

```bash theme={null}
gcloud sql instances create $SQL_INSTANCE \
  --database-version=POSTGRES_16 \
  --tier=db-custom-2-16384 \
  --region=$GCP_REGION \
  --storage-size=100GB \
  --storage-type=SSD \
  --storage-auto-increase \
  --no-assign-ip
```

### Enable IAM Authentication on the Instance

```bash theme={null}
gcloud sql instances patch $SQL_INSTANCE \
  --database-flags=cloudsql.iam_authentication=on
```

<Warning>
  `cloudsql.iam_authentication` is **off by default**. The Cloud SQL Auth Proxy cannot authenticate via IAM tokens until this flag is enabled — pods will fail with authentication errors.
</Warning>

### Create the Four Required Databases

```bash theme={null}
gcloud sql databases create crewai_plus_production --instance=$SQL_INSTANCE
gcloud sql databases create crewai_plus_cable_production --instance=$SQL_INSTANCE
gcloud sql databases create crewai_plus_oauth_production --instance=$SQL_INSTANCE
gcloud sql databases create wharf --instance=$SQL_INSTANCE
```

<Warning>
  The database names above must be used exactly as shown. In particular, `crewai_plus_oauth_production` overrides the chart default of `oauth_db`. You must set `POSTGRES_OAUTH_DB: "crewai_plus_oauth_production"` in `envVars:` — if omitted, the OAuth service attempts to connect to a database that does not exist and fails silently.
</Warning>

### Create the IAM Database User

```bash theme={null}
gcloud sql users create ${GSA_NAME}@${GCP_PROJECT_ID}.iam \
  --instance=$SQL_INSTANCE \
  --type=CLOUD_IAM_SERVICE_ACCOUNT
```

### Grant SQL Privileges

Connect to the instance as the `postgres` superuser:

```bash theme={null}
gcloud sql connect $SQL_INSTANCE --user=postgres --database=postgres
```

Then run the following SQL (replace `GSA_NAME@GCP_PROJECT_ID.iam` with your actual IAM user, e.g., `crewai-platform@my-project.iam`):

```sql theme={null}
-- Grant connect privileges
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_production TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_cable_production TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_oauth_production TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL PRIVILEGES ON DATABASE wharf TO "GSA_NAME@GCP_PROJECT_ID.iam";

-- Schema and table privileges for each database
\c crewai_plus_production
GRANT ALL ON SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "GSA_NAME@GCP_PROJECT_ID.iam";

\c crewai_plus_cable_production
GRANT ALL ON SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "GSA_NAME@GCP_PROJECT_ID.iam";

\c crewai_plus_oauth_production
GRANT ALL ON SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "GSA_NAME@GCP_PROJECT_ID.iam";

\c wharf
GRANT ALL ON SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "GSA_NAME@GCP_PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "GSA_NAME@GCP_PROJECT_ID.iam";
```

***

## Part 3: GCS and Artifact Registry

### Create GCS Bucket

```bash theme={null}
gsutil mb -p $GCP_PROJECT_ID -l $GCP_REGION -b on gs://$GCS_BUCKET
gsutil versioning set on gs://$GCS_BUCKET
```

### Create Artifact Registry Repository

```bash theme={null}
gcloud artifacts repositories create $AR_REPO \
  --repository-format=docker \
  --location=$GCP_REGION \
  --description="CrewAI Platform crew images"
```

The `CREW_IMAGE_REGISTRY_OVERRIDE` value for this repository is:
`${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${AR_REPO}`

For example: `us-central1-docker.pkg.dev/my-project/crewai`

<Warning>
  Do NOT include `/crewai-enterprise` in `CREW_IMAGE_REGISTRY_OVERRIDE`. The platform appends `/crewai-enterprise` automatically. Including the suffix causes image push failures to a double-suffixed path.

  Also ensure the Artifact Registry repository has **mutable tags**. CrewAI overwrites image tags for crew versions — immutable tags will cause build failures.
</Warning>

### Enable Gateway API on the Cluster

```bash theme={null}
gcloud container clusters update $GKE_CLUSTER \
  --gateway-api=standard \
  --region=$GCP_REGION \
  --project=$GCP_PROJECT_ID
```

***

## Part 4: Microsoft Entra ID App Registration

### Create the App Registration

1. Go to [portal.azure.com](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations → New registration**
2. Set the application name (suggested: `CrewAI`)
3. Under **Supported account types**, select **Accounts in this organizational directory only**
4. Under **Redirect URI**, select platform **Web** and enter: `https://<YOUR_DOMAIN>/auth/entra_id/callback`
5. Click **Register**

<Warning>
  The redirect URI must be registered in Azure **before** running `helm install`. The exact format required is `https://<YOUR_DOMAIN>/auth/entra_id/callback`. Authentication will fail silently at login if this is missing or incorrect.
</Warning>

### Collect Credentials

From the app's **Overview** page, copy:

* **Application (client) ID** → this is `ENTRA_ID_CLIENT_ID`
* **Directory (tenant) ID** → this is `ENTRA_ID_TENANT_ID`

### Create a Client Secret

1. In the left sidebar under **Manage**, click **Certificates & secrets**
2. Click **New client secret**, set a description and expiration, then click **Add**
3. Copy the secret **Value** immediately — you cannot view it again after leaving the page
4. This is `ENTRA_ID_CLIENT_SECRET`

### Grant Admin Consent

1. Navigate to **Enterprise applications → All applications** and select your app
2. In the left sidebar under **Security**, click **Permissions**
3. Click **Grant admin consent for \<your organization>**
4. Confirm that Microsoft Graph **User.Read** is granted

### Configure App Roles

In the left sidebar under **Manage**, click **App roles**, then **Create app role**. Create both of the following roles:

| Display name  | Value           | Allowed member types |
| ------------- | --------------- | -------------------- |
| Member        | `member`        | Users/Groups         |
| Factory Admin | `factory-admin` | Users/Groups         |

Ensure **"Do you want to enable this app role?"** is checked for each role before saving.

### Assign Users

1. Go to **Enterprise applications → \<your app> → Users and groups**
2. Click **Add user/group**
3. Assign regular users the **Member** role
4. Assign admin users the **Factory Admin** role

<Note>
  With Entra ID, admin access is granted via the `factory-admin` App Role in Azure. Do **not** run the `factory:grant_admin` Rails task — that command is for WorkOS/Okta/local auth only. For Entra ID, assign the `factory-admin` App Role to the user in the Azure portal instead.
</Note>

***

## Part 5: Complete values.yaml

The following is a complete, production-ready `values.yaml`. Fill in all `<PLACEHOLDER>` values before deploying.

**Generate your `SECRET_KEY_BASE`:**

```bash theme={null}
openssl rand -base64 64 | tr -d '\n'
```

```yaml values.yaml theme={null}
# ============================================================
# CrewAI Platform — GCP GKE + Entra ID + Wharf + Studio V2
# ============================================================

# ServiceAccount annotated for Workload Identity
serviceAccount:
  annotations:
    iam.gke.io/gcp-service-account: crewai-platform@<YOUR_GCP_PROJECT_ID>.iam.gserviceaccount.com

# Namespace for crew workloads
crewNamespace: "crewai-crews"

# Image pull credentials for images.crewai.com (Replicated proxy)
# Required for direct Helm installs — use your license email and token
image:
  registries:
    - host: "images.crewai.com"
      username: "<YOUR_LICENSE_EMAIL>"
      password: "<YOUR_REPLICATED_LICENSE_TOKEN>"

# Disable built-in PostgreSQL — using Cloud SQL
postgres:
  enabled: false

# Disable built-in MinIO — using GCS
minio:
  enabled: false

# -------------------------------------------------------
# Cloud SQL Auth Proxy (sidecar on web, worker, job pods)
# -------------------------------------------------------
cloudSqlProxy:
  enabled: true
  instanceConnectionName: "<YOUR_GCP_PROJECT_ID>:<YOUR_REGION>:<YOUR_SQL_INSTANCE>"  # e.g. my-project:us-central1:crewai-db
  port: 5432
  privateIp: true        # Use private IP for VPC-peered instances (recommended)
  autoIamAuthn: true     # IAM-based auth — no DB_PASSWORD needed

# -------------------------------------------------------
# Environment Variables (non-sensitive configuration)
# -------------------------------------------------------
envVars:
  # --- Database (Cloud SQL Auth Proxy) ---
  DB_HOST: "127.0.0.1"                        # Proxy runs as localhost sidecar
  DB_PORT: "5432"
  DB_USER: "crewai-platform@<YOUR_GCP_PROJECT_ID>.iam"  # GSA email WITHOUT .gserviceaccount.com suffix
  POSTGRES_DB: "crewai_plus_production"
  POSTGRES_CABLE_DB: "crewai_plus_cable_production"
  POSTGRES_OAUTH_DB: "crewai_plus_oauth_production"     # Must override — chart default is "oauth_db"

  # --- GCS Object Storage ---
  STORAGE_SERVICE: "google"
  GCS_PROJECT_ID: "<YOUR_GCP_PROJECT_ID>"
  GCS_BUCKET: "<YOUR_GCS_BUCKET>"                       # e.g. crewai-prod-storage
  GCS_IAM_SIGNING: "true"                               # REQUIRED with Workload Identity — omitting causes SignedUrlUnavailable errors

  # --- Artifact Registry (crew images) ---
  CREW_IMAGE_REGISTRY_OVERRIDE: "<YOUR_REGION>-docker.pkg.dev/<YOUR_GCP_PROJECT_ID>/<YOUR_AR_REPO>"
  # Example: us-central1-docker.pkg.dev/my-project/crewai
  # Do NOT include /crewai-enterprise — the platform appends it automatically

  # --- Entra ID SSO ---
  AUTH_PROVIDER: "entra_id"                             # Exact value required — lowercase, underscore
  ENTRA_ID_CLIENT_ID: "<APPLICATION_CLIENT_ID>"         # From Azure App Registration Overview
  ENTRA_ID_TENANT_ID: "<DIRECTORY_TENANT_ID>"           # From Azure App Registration Overview
  # Note: CLIENT_ID and TENANT_ID are non-sensitive identifiers — they belong here in envVars, NOT in secrets

  # --- Application ---
  APPLICATION_HOST: "<YOUR_DOMAIN>"                     # e.g. crewai.your-company.com (no https://)
  RAILS_LOG_LEVEL: "info"

# -------------------------------------------------------
# Secrets (sensitive credentials)
# -------------------------------------------------------
secrets:
  SECRET_KEY_BASE: "<OUTPUT_OF: openssl rand -base64 64 | tr -d '\n'>"

  # Entra ID client secret — belongs here in secrets (not envVars)
  ENTRA_ID_CLIENT_SECRET: "<CLIENT_SECRET_VALUE>"       # From Azure Certificates & Secrets

  # DB_PASSWORD is NOT needed when autoIamAuthn: true — the proxy handles auth via Workload Identity

# -------------------------------------------------------
# Wharf (OTLP trace collection) — enabled by default
# -------------------------------------------------------
wharf:
  enabled: true          # Explicit for clarity — this is the chart default

# -------------------------------------------------------
# Gateway API
# -------------------------------------------------------
gateway:
  enabled: true
  create: true
  gatewayClassName: gke-l7-regional-external-managed  # Use gke-l7-global-external-managed for multi-region or CDN
  annotations:
    networking.gke.io/certmap: "<YOUR_CERT_MAP_NAME>"   # GCP-managed certificate map
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
    - name: http
      protocol: HTTP
      port: 80

# -------------------------------------------------------
# Web
# Note: Merge all web: settings under this single top-level web: key.
# Helm silently drops all but the last occurrence of a duplicate top-level key.
# -------------------------------------------------------
web:
  replicaCount: 3
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"
  gateway:
    enabled: true
    hostnames:
      - "<YOUR_DOMAIN>"   # Must match APPLICATION_HOST above

# -------------------------------------------------------
# Worker
# -------------------------------------------------------
worker:
  replicaCount: 3
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"

# -------------------------------------------------------
# BuildKit (crew image builds)
# -------------------------------------------------------
buildkit:
  enabled: true
  replicaCount: 1
  resources:
    requests:
      cpu: "500m"
      memory: "2Gi"
    limits:
      cpu: "4"
      memory: "8Gi"

# -------------------------------------------------------
# OAuth microservice (Built-In Integrations — optional)
# Remove this block if you do not need tool integrations
# (Microsoft 365, Google Workspace, HubSpot, etc.)
# -------------------------------------------------------
oauth:
  enabled: true
  gateway:
    enabled: true
    pathPrefix: "/oauthsvc"

# -------------------------------------------------------
# RBAC
# -------------------------------------------------------
rbac:
  create: true
```

<Note>
  The `image.registries` block is required for direct Helm installs. It provides credentials for pulling platform images (busybox, redis, buildkit, wharf, etc.) from the Replicated proxy at `images.crewai.com`. Use the same email and token used for `helm registry login registry.crewai.com`.

  When installing via Replicated KOTS, these credentials are injected automatically — `image.registries` is not needed.
</Note>

<Warning>
  **`DB_USER` format for Cloud SQL IAM authentication:** Set this to the GSA email **without** the `.gserviceaccount.com` suffix. The Cloud SQL Auth Proxy strips this suffix during IAM token exchange. Including the full suffix causes authentication to fail silently.

  * Correct: `crewai-platform@my-project.iam`
  * Wrong: `crewai-platform@my-project.iam.gserviceaccount.com`
</Warning>

***

## Part 6: Install

### Create GCP-Managed TLS Certificate (if using certmap)

```bash theme={null}
gcloud certificate-manager certificates create crewai-cert \
  --domains="<YOUR_DOMAIN>"

gcloud certificate-manager maps create crewai-cert-map

gcloud certificate-manager maps entries create crewai-cert-entry \
  --map=crewai-cert-map \
  --certificates=crewai-cert \
  --hostname="<YOUR_DOMAIN>"
```

### Log In to the Helm Registry

```bash theme={null}
helm registry login registry.crewai.com \
  --username "<YOUR_LICENSE_EMAIL>" \
  --password "<YOUR_REPLICATED_LICENSE_TOKEN>"
```

### Run Helm Install

```bash theme={null}
helm install crewai-platform \
  oci://registry.crewai.com/crewai/stable/crewai-platform \
  --values values.yaml \
  --namespace crewai \
  --create-namespace
```

### Annotate the Crews Namespace (Post-Install, Required)

After the first install creates the `crewai-crews` namespace, annotate the default ServiceAccount so build pods can push images to Artifact Registry via Workload Identity:

```bash theme={null}
kubectl annotate serviceaccount default -n crewai-crews --overwrite \
  iam.gke.io/gcp-service-account=${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com
```

<Warning>
  Until this annotation is applied, all crew image builds will fail with a permission denied error when pushing to Artifact Registry.
</Warning>

***

## Part 7: Post-Install (Required for All Deployments)

Wait for all pods to reach `Running` status, then run the following commands:

```bash theme={null}
# Initialize the internal organization
kubectl exec -it deploy/crewai-web -- bin/rails studio:install_internal_organization

# Set up default permissions
kubectl exec -it deploy/crewai-web -- bin/rails factory:setup_permissions_defaults

# Add the initial platform owner (replace with your admin's email)
kubectl exec -it deploy/crewai-web -- bin/rails 'factory:add_owner[2,admin@your-company.com]'
```

<Note>
  For Entra ID deployments, do **not** run `factory:grant_admin`. Admin access is controlled entirely by the `factory-admin` App Role assigned in the Azure portal. Running `factory:grant_admin` is only needed for WorkOS, Okta, and local auth deployments.
</Note>

***

## Part 8: Studio V2 Setup (Post-Install)

Studio V2 has no Helm values and cannot be configured in `values.yaml`. Adding `studioV2.enabled`, `STUDIO_V2_ENABLED`, or any similar key to your values has no effect — Helm silently ignores unrecognized keys.

Setup is always performed post-install in this order:

### Step 1: Create the LLM Connection (UI)

1. Log in to the platform at `https://<YOUR_DOMAIN>`
2. Navigate to **Settings → LLM Connections**
3. Click **New Connection**
4. Set the name to exactly `studio-v2` (lowercase, no spaces — this exact string is required)
5. Select your LLM provider, enter the model name and API key
6. Click **Save**

<Warning>
  The connection name must be exactly `studio-v2`. The install commands in Step 3 look up this name specifically — any variation in spelling or capitalization will cause them to fail.
</Warning>

### Step 2: Set as Default Connection (UI)

1. Navigate to **Settings → Crew Studio**
2. Under **Default Connection**, select `studio-v2`
3. Click **Save**

### Step 3: Run Install Commands (kubectl)

Run these commands **in order**. Each must complete successfully before running the next.

```bash theme={null}
# 1. Install the Studio agent (requires studio-v2 LLM Connection to exist)
kubectl exec -it deploy/crewai-web -- bin/rails studio:agent:install

# 2. Sync tools
kubectl exec -it deploy/crewai-web -- \
  bin/rails studio:tools:sync_crewai_tools studio:tools:sync_enterprise_tools

# 3. Install the Studio runner
kubectl exec -it deploy/crewai-web -- bin/rails studio:runner:install
```

<Warning>
  `studio:agent:install` will fail if the `studio-v2` LLM Connection does not already exist. Complete Steps 1 and 2 before running any kubectl commands.
</Warning>

***

## Part 9: Verify

### Platform Health

```bash theme={null}
# Check all pods are running
kubectl get pods -n crewai

# Check web application health
kubectl exec -it deploy/crewai-web -- bin/rails runner "puts 'OK'"

# Verify Wharf is running
kubectl get pods -n crewai -l app.kubernetes.io/component=wharf
```

### Gateway and DNS

```bash theme={null}
# Get the Gateway external IP — update your DNS A record to point to it
kubectl get gateway -n crewai

# Verify the application is reachable
curl -I https://<YOUR_DOMAIN>/health
```

### Entra ID Authentication

1. Open `https://<YOUR_DOMAIN>` in a browser
2. Click **Sign in** — you should be redirected to the Microsoft login page
3. Log in with an Entra ID account that has been assigned either the **Member** or **Factory Admin** App Role
4. After login, verify you are redirected back to the platform and successfully authenticated

### Wharf Trace Collection

```bash theme={null}
# Verify Wharf database tables were created
kubectl exec -it deploy/crewai-web -- \
  bin/rails runner "puts ActiveRecord::Base.connection.tables.grep(/wharf/).inspect"

# Check Wharf pod logs
kubectl logs -l app.kubernetes.io/component=wharf --tail=30 -n crewai
```

### Studio V2

```bash theme={null}
# Verify Studio deployments are running
kubectl get deploy -n crewai | grep studio

# Check Studio assistant logs
kubectl logs -l app=studio-assistant --tail=20 -n crewai
```

Both the `studio-assistant` and `studio-runner` deployments should show `Running`.

***

## Troubleshooting

### Entra ID: Silent Auth Failure (No Error, Login Loops)

**Cause:** `ENTRA_ID_CLIENT_ID` or `ENTRA_ID_TENANT_ID` were placed under `secrets:` instead of `envVars:`.

The chart template does not inject Entra ID values from the `secrets:` section into pod environment variables. The pod starts normally but cannot authenticate because the identifiers are absent from the environment.

**Fix:** Move `ENTRA_ID_CLIENT_ID` and `ENTRA_ID_TENANT_ID` to `envVars:`. Only `ENTRA_ID_CLIENT_SECRET` belongs under `secrets:`.

### Entra ID: "Redirect URI mismatch" Error

**Cause:** The redirect URI registered in Azure does not exactly match `https://<YOUR_DOMAIN>/auth/entra_id/callback`.

**Fix:** In the Azure portal, go to **App registrations → \<your app> → Authentication** and verify the redirect URI matches exactly, including the `https://` prefix, exact domain, and the path `/auth/entra_id/callback`.

### Cloud SQL: Auth Proxy Authentication Failure

**Symptoms:** Pods show `issue connecting with your username/password` or `fe_sendauth: no password supplied`.

1. Verify `autoIamAuthn: true` is set in `cloudSqlProxy:`
2. Verify `cloudsql.iam_authentication=on` is set on the Cloud SQL instance:
   ```bash theme={null}
   gcloud sql instances describe $SQL_INSTANCE \
     --format='value(settings.databaseFlags)'
   ```
3. Verify the IAM database user exists:
   ```bash theme={null}
   gcloud sql users list --instance=$SQL_INSTANCE --format="table(name,type)"
   ```
4. Verify `DB_USER` uses the truncated format (without `.gserviceaccount.com`)

### GCS: SignedUrlUnavailable Error

**Symptoms:** Logs show `Google::Cloud::Storage::SignedUrlUnavailable`.

**Fix:** Ensure `GCS_IAM_SIGNING: "true"` is set in `envVars:` and the GSA has `roles/iam.serviceAccountTokenCreator`. This setting is required whenever Workload Identity is used.

### Cloud SQL: Missing OAuth Database

**Symptoms:** OAuth service fails to start; logs show database does not exist.

**Fix:** Verify `POSTGRES_OAUTH_DB: "crewai_plus_oauth_production"` is set in `envVars:`. The chart default is `oauth_db`. If you created the database as `crewai_plus_oauth_production` but did not override this value, the service connects to the wrong database name.

### Studio V2: `studio:agent:install` Fails

**Cause:** The `studio-v2` LLM Connection does not exist in the database yet.

**Fix:** Complete the UI steps (Settings → LLM Connections → New Connection named exactly `studio-v2`, then Settings → Crew Studio → set as Default Connection) before running any Rails tasks.

### Artifact Registry: Build Pod Push Failures

**Symptoms:** Crew image builds fail with `unauthorized` or `denied` during push.

**Fix:** Verify the Workload Identity annotation on the crews namespace ServiceAccount:

```bash theme={null}
kubectl get serviceaccount default -n crewai-crews -o jsonpath='{.metadata.annotations}'
```

If missing, re-run the annotation command from Part 6.

### Workload Identity Not Working

**Verify the cluster has Workload Identity enabled:**

```bash theme={null}
gcloud container clusters describe $GKE_CLUSTER \
  --region=$GCP_REGION \
  --format='value(workloadIdentityConfig.workloadPool)'
# Expected: <GCP_PROJECT_ID>.svc.id.goog
```

**Verify the ServiceAccount annotation:**

```bash theme={null}
kubectl get serviceaccount $KSA_NAME -n $GKE_NAMESPACE -o yaml | grep gcp-service-account
```

**Test from a pod:**

```bash theme={null}
kubectl run -it --rm wi-test \
  --namespace=$GKE_NAMESPACE \
  --image=google/cloud-sdk:slim \
  --serviceaccount=$KSA_NAME \
  --restart=Never -- \
  gcloud auth print-access-token
```
