Coolify migrating resources between teams

Coolify migrating resources between teams

🚨 Before executing any scripts, make sure to create a full database dump first! Run a backup of the Coolify PostgreSQL database before proceeding with any migration or modification scripts. In case of an error, this backup is your only way to restore the previous state.
🚨 Before executing any scripts, make sure to create a full database dump first! Run a backup of the Coolify PostgreSQL database before proceeding with any migration or modification scripts. In case of an error, this backup is your only way to restore the previous state.
🚨 Before executing any scripts, make sure to create a full database dump first! Run a backup of the Coolify PostgreSQL database before proceeding with any migration or modification scripts. In case of an error, this backup is your only way to restore the previous state.

Kacper Zawojski

Software Engineer (Fullstack)

Kacper Zawojski is a full-stack developer and PayloadCMS expert with a background in algorithmic computer science from Wrocław University of Technology and a master's in Mechatronics and Robotics from Warsaw University of Technology. He's been building production web products at WAYF since early 2024, working across React, TypeScript, Next.js, and Payload — the stack that powers most of our client work.

0. Pre-flight: Database backup

Before running any migration scripts, create a full dump of the Coolify database. The SSH tunnel from section 1 must be running on port 5433.

0.1 Create a dump

Run from your local machine (requires the SSH tunnel from section 1.3 to be active):

pg_dump \\
  --host=localhost \\
  --port=5433 \\
  --username=<DB_USER> \\
  --dbname=coolify \\
  --schema=public \\
  --no-owner \\
  --no-privileges \\
  --format=custom \\
  --verbose \\
  --file=coolify_backup_$(date +%Y-%m-%d)

pg_dump \\
  --host=localhost \\
  --port=5433 \\
  --username=<DB_USER> \\
  --dbname=coolify \\
  --schema=public \\
  --no-owner \\
  --no-privileges \\
  --format=custom \\
  --verbose \\
  --file=coolify_backup_$(date +%Y-%m-%d)

pg_dump \\
  --host=localhost \\
  --port=5433 \\
  --username=<DB_USER> \\
  --dbname=coolify \\
  --schema=public \\
  --no-owner \\
  --no-privileges \\
  --format=custom \\
  --verbose \\
  --file=coolify_backup_$(date +%Y-%m-%d)

💡Replace <DB_USER> with the value obtained in step 1.4. You will be prompted for the password.

0.2 Copy the dump to/from the server

Copy file from the server to your local machine:

scp -i
scp -i
scp -i

Copy file from your local machine to the server:

scp -i
scp -i
scp -i

0.3 Restore the dump to a local Docker database

Run from your local machine:

docker run --rm \\
  --network <your-local-network> \\
  -e PGPASSWORD=<local-db-password> \\
  -v "$PWD":/backup \\
  postgres:17 \\
  bash -c "pg_restore \\
    -h <local-postgres-container-name> \\
    -U postgres \\
    -d coolify \\
    --clean \\
    --if-exists \\
    --no-owner \\
    --no-privileges \\
    --verbose \\
    /backup/coolify_backup.dump 2>&1 | grep -v 'transaction_timeout' || true"
docker run --rm \\
  --network <your-local-network> \\
  -e PGPASSWORD=<local-db-password> \\
  -v "$PWD":/backup \\
  postgres:17 \\
  bash -c "pg_restore \\
    -h <local-postgres-container-name> \\
    -U postgres \\
    -d coolify \\
    --clean \\
    --if-exists \\
    --no-owner \\
    --no-privileges \\
    --verbose \\
    /backup/coolify_backup.dump 2>&1 | grep -v 'transaction_timeout' || true"
docker run --rm \\
  --network <your-local-network> \\
  -e PGPASSWORD=<local-db-password> \\
  -v "$PWD":/backup \\
  postgres:17 \\
  bash -c "pg_restore \\
    -h <local-postgres-container-name> \\
    -U postgres \\
    -d coolify \\
    --clean \\
    --if-exists \\
    --no-owner \\
    --no-privileges \\
    --verbose \\
    /backup/coolify_backup.dump 2>&1 | grep -v 'transaction_timeout' || true"

💡If you see an error about a missing ROLE (e.g. master), create it first in your local database:

CREATE ROLE master;

This document describes how to connect to the Coolify PostgreSQL database, the data model, and procedures for migrating projects and servers between teams (either an entire team or a single project). It includes SQL templates and examples.

1. Connecting to the Coolify database (production)

The Coolify PostgreSQL database runs in a Docker container on the server and is not exposed to the internet. Access is via an SSH tunnel.

1.1 Requirements

  • SSH key with access to the server (e.g. ~/.ssh/id_rsa or your preferred key)

  • SSH access to the server as root

1.2 Step 1: Database container IP on the server

ssh root@xx.xxx.xx.xxx "docker ps | grep -i postgres"
ssh root@xx.xxx.xx.xxx "docker ps | grep -i postgres"
ssh root@xx.xxx.xx.xxx "docker ps | grep -i postgres"

Option A — Docker Go template (make sure the `` brackets are not lost when copying):

docker inspect coolify-db --format 'range .NetworkSettings.Networks.IPAddressend'
docker inspect coolify-db --format 'range .NetworkSettings.Networks.IPAddressend'
docker inspect coolify-db --format 'range .NetworkSettings.Networks.IPAddressend'

Note the container IP (e.g. xxx.xx.x.x). The database is not mapped to a host port, so the tunnel must target the container IP.

1.3 Step 2: SSH tunnel

Example (container IP xxx.xx.x.x):

ssh -N -L 5433
ssh -N -L 5433
ssh -N -L 5433

Keep the terminal open.

1.4 Step 3: Database credentials

ssh root@xx.xxx.xx.xxx "cat /data/coolify/source/.env | grep -i -E 'DB_|POSTGRES'"
ssh root@xx.xxx.xx.xxx "cat /data/coolify/source/.env | grep -i -E 'DB_|POSTGRES'"
ssh root@xx.xxx.xx.xxx "cat /data/coolify/source/.env | grep -i -E 'DB_|POSTGRES'"

Alternatively (Docker):

ssh root@xx.xxx.xx.xxx "docker inspect coolify-db | grep -i -E 'POSTGRES_PASSWORD|POSTGRES_USER|POSTGRES_DB'"
ssh root@xx.xxx.xx.xxx "docker inspect coolify-db | grep -i -E 'POSTGRES_PASSWORD|POSTGRES_USER|POSTGRES_DB'"
ssh root@xx.xxx.xx.xxx "docker inspect coolify-db | grep -i -E 'POSTGRES_PASSWORD|POSTGRES_USER|POSTGRES_DB'"

1.5 Step 4: Connecting in a database client (VS Code / Database Client)

Create a new connection without using the client’s built-in SSH (the tunnel is already running in the terminal):

Parameter

Value

Host

localhost

Port

5433

User

(from step 3)

Password

(from step 3)

Database

coolify

2. Database relation structure

Resources are linked to a team either directly (team_id) or through the hierarchy project → environment.




The applications, services, and standalone_* tables do not have a team_id column — team membership is derived from:

environment_idenvironments.project_idprojects.team_id.

Moving a project is done by updating projects.team_id and any related resources that have their own team_id (servers, keys, backups, etc.).

3. Tables with a direct team_id

Table

Notes

projects

Main table — moving it moves environments and everything under them

servers

Servers hosting resources

private_keys

SSH keys

github_apps

GitHub integrations

gitlab_apps

GitLab integrations

s3_storages

Backup storage

tags

Resource tags

scheduled_database_backups

Scheduled backups

scheduled_tasks

Cron tasks

shared_environment_variables

Shared env variables

discord_notification_settings

Discord notifications

email_notification_settings

Email notifications

slack_notification_settings

Slack notifications

telegram_notification_settings

Telegram notifications

pushover_notification_settings

Pushover notifications

subscriptions

Stripe subscriptions

team_user

Team ↔ user pivot (optional)

personal_access_tokens

API tokens (team_id is varchar)

4. Migration scenarios

4.1 Migrating an entire team A → B

All team resources are moved (projects, servers, keys, integrations, backups, notifications, etc.). Use the template in 5.1.

4.2 Migrating only resources tied to a single project

Only the following are moved:

  • The chosen project (projects.id = X);

  • Servers that are the destination for applications, services, or standalone DBs in that project’s environments (with optional exclusions, e.g. localhost);

  • Private keys, GitHub/GitLab apps, S3, tags, scheduled backups/tasks, shared env vars — only those linked to that project.

Notification settings, subscriptions, and personal_access_tokens are not moved (they stay at team level).

Templates: dry-run 5.2, migration 5.3.

4.3 Reverting a single server to its previous team

E.g. the “localhost” server (id=0) should remain in team 0 despite the project migration. Template: 6.1.

5. Scripts and templates

5.1 Template: Migrate entire team A → B

Note: Replace old_team and new_team with the correct IDs. Back up the database before running and run the state check query (5.4) first.

BEGIN;

DO $$
DECLARE
  old_team bigint := 0;   -- <-- CHANGE: source team ID
  new_team bigint := 1;   -- <-- CHANGE: target team ID
  env_ids  bigint[];
  proj_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO proj_ids FROM projects WHERE team_id = old_team;
  UPDATE projects SET team_id = new_team WHERE team_id = old_team;

  UPDATE servers SET team_id = new_team WHERE team_id = old_team;

  SELECT array_agg(id) INTO env_ids
  FROM environments WHERE project_id = ANY(proj_ids);

  UPDATE private_keys SET team_id = new_team WHERE team_id = old_team;
  UPDATE github_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE gitlab_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE s3_storages SET team_id = new_team WHERE team_id = old_team;
  UPDATE tags SET team_id = new_team WHERE team_id = old_team;

  UPDATE scheduled_database_backups SET team_id = new_team WHERE team_id = old_team;
  UPDATE scheduled_tasks SET team_id = new_team WHERE team_id = old_team;

  UPDATE shared_environment_variables SET team_id = new_team WHERE team_id = old_team;

  UPDATE discord_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE email_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE slack_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE telegram_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE pushover_notification_settings SET team_id = new_team WHERE team_id = old_team;

  UPDATE subscriptions SET team_id = new_team WHERE team_id = old_team;

  UPDATE personal_access_tokens SET team_id = new_team::text WHERE team_id = old_team::text;

  -- Optional: move team members
  -- UPDATE team_user SET team_id = new_team WHERE team_id = old_team;

  RAISE NOTICE 'Moved resources from team % to team %', old_team, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects;
-- SELECT id, name, team_id FROM servers;

COMMIT;
-- On failure: ROLLBACK;
BEGIN;

DO $$
DECLARE
  old_team bigint := 0;   -- <-- CHANGE: source team ID
  new_team bigint := 1;   -- <-- CHANGE: target team ID
  env_ids  bigint[];
  proj_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO proj_ids FROM projects WHERE team_id = old_team;
  UPDATE projects SET team_id = new_team WHERE team_id = old_team;

  UPDATE servers SET team_id = new_team WHERE team_id = old_team;

  SELECT array_agg(id) INTO env_ids
  FROM environments WHERE project_id = ANY(proj_ids);

  UPDATE private_keys SET team_id = new_team WHERE team_id = old_team;
  UPDATE github_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE gitlab_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE s3_storages SET team_id = new_team WHERE team_id = old_team;
  UPDATE tags SET team_id = new_team WHERE team_id = old_team;

  UPDATE scheduled_database_backups SET team_id = new_team WHERE team_id = old_team;
  UPDATE scheduled_tasks SET team_id = new_team WHERE team_id = old_team;

  UPDATE shared_environment_variables SET team_id = new_team WHERE team_id = old_team;

  UPDATE discord_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE email_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE slack_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE telegram_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE pushover_notification_settings SET team_id = new_team WHERE team_id = old_team;

  UPDATE subscriptions SET team_id = new_team WHERE team_id = old_team;

  UPDATE personal_access_tokens SET team_id = new_team::text WHERE team_id = old_team::text;

  -- Optional: move team members
  -- UPDATE team_user SET team_id = new_team WHERE team_id = old_team;

  RAISE NOTICE 'Moved resources from team % to team %', old_team, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects;
-- SELECT id, name, team_id FROM servers;

COMMIT;
-- On failure: ROLLBACK;
BEGIN;

DO $$
DECLARE
  old_team bigint := 0;   -- <-- CHANGE: source team ID
  new_team bigint := 1;   -- <-- CHANGE: target team ID
  env_ids  bigint[];
  proj_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO proj_ids FROM projects WHERE team_id = old_team;
  UPDATE projects SET team_id = new_team WHERE team_id = old_team;

  UPDATE servers SET team_id = new_team WHERE team_id = old_team;

  SELECT array_agg(id) INTO env_ids
  FROM environments WHERE project_id = ANY(proj_ids);

  UPDATE private_keys SET team_id = new_team WHERE team_id = old_team;
  UPDATE github_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE gitlab_apps SET team_id = new_team WHERE team_id = old_team;
  UPDATE s3_storages SET team_id = new_team WHERE team_id = old_team;
  UPDATE tags SET team_id = new_team WHERE team_id = old_team;

  UPDATE scheduled_database_backups SET team_id = new_team WHERE team_id = old_team;
  UPDATE scheduled_tasks SET team_id = new_team WHERE team_id = old_team;

  UPDATE shared_environment_variables SET team_id = new_team WHERE team_id = old_team;

  UPDATE discord_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE email_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE slack_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE telegram_notification_settings SET team_id = new_team WHERE team_id = old_team;
  UPDATE pushover_notification_settings SET team_id = new_team WHERE team_id = old_team;

  UPDATE subscriptions SET team_id = new_team WHERE team_id = old_team;

  UPDATE personal_access_tokens SET team_id = new_team::text WHERE team_id = old_team::text;

  -- Optional: move team members
  -- UPDATE team_user SET team_id = new_team WHERE team_id = old_team;

  RAISE NOTICE 'Moved resources from team % to team %', old_team, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects;
-- SELECT id, name, team_id FROM servers;

COMMIT;
-- On failure: ROLLBACK;

5.2 Template: Dry-run — migrate single project

Read-only queries — they show what would be moved for one project to the chosen team. Replace 10 (project_id) and 11 (new_team) with your values.

Team state:

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

Verify project and target team:

SELECT id, name, team_id FROM projects WHERE id = 10;
SELECT id, name FROM teams WHERE id = 11

SELECT id, name, team_id FROM projects WHERE id = 10;
SELECT id, name FROM teams WHERE id = 11

SELECT id, name, team_id FROM projects WHERE id = 10;
SELECT id, name FROM teams WHERE id = 11

Servers linked to the project (example for project_id = 10):

WITH envs AS (SELECT id FROM environments WHERE project_id = 10),
dest_servers AS (
  SELECT sd.server_id FROM applications a
  JOIN envs e ON e.id = a.environment_id
  JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM applications a JOIN envs e ON e.id = a.environment_id
  JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
)
SELECT s.id, s.name, s.team_id FROM dest_servers ds
JOIN servers s ON s.id = ds.server_id
ORDER BY

WITH envs AS (SELECT id FROM environments WHERE project_id = 10),
dest_servers AS (
  SELECT sd.server_id FROM applications a
  JOIN envs e ON e.id = a.environment_id
  JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM applications a JOIN envs e ON e.id = a.environment_id
  JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
)
SELECT s.id, s.name, s.team_id FROM dest_servers ds
JOIN servers s ON s.id = ds.server_id
ORDER BY

WITH envs AS (SELECT id FROM environments WHERE project_id = 10),
dest_servers AS (
  SELECT sd.server_id FROM applications a
  JOIN envs e ON e.id = a.environment_id
  JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM applications a JOIN envs e ON e.id = a.environment_id
  JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM services srv JOIN envs e ON e.id = srv.environment_id
  JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
  UNION
  SELECT sd.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
  UNION
  SELECT sw.server_id FROM standalone_postgresqls sp JOIN envs e ON e.id = sp.environment_id
  JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
)
SELECT s.id, s.name, s.team_id FROM dest_servers ds
JOIN servers s ON s.id = ds.server_id
ORDER BY

Use the dry-run to decide which servers to exclude (e.g. localhost) before running the actual migration.

5.3 Template: Migrate single project (actual migration)

Moves: the chosen project, only the related servers (with optional exclusion of e.g. server_id = 0), private keys, GitHub/GitLab apps, S3, tags, scheduled backups/tasks, and shared env vars tied to that project.

Parameters to set at the start of the block:

  • project_id_to_move — ID of the project to move

  • old_team — current team (e.g. 0)

  • new_team — target team (e.g. 11)

BEGIN;

DO $$
DECLARE
  project_id_to_move bigint := 10;   -- <-- CHANGE
  old_team bigint := 0;              -- <-- CHANGE
  new_team bigint := 11;             -- <-- CHANGE
  env_ids bigint[];
  server_ids bigint[];
  key_ids bigint[];
  github_ids bigint[];
  gitlab_ids bigint[];
  s3_ids bigint[];
  tag_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO env_ids FROM environments WHERE project_id = project_id_to_move;

  UPDATE projects SET team_id = new_team WHERE id = project_id_to_move AND team_id = old_team;

  WITH dest_servers AS (
    SELECT sd.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
    UNION SELECT sw.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
    UNION SELECT sd.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
    UNION SELECT sw.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
    UNION SELECT sd.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
    UNION SELECT sw.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
  )
  SELECT array_agg(DISTINCT server_id) INTO server_ids FROM dest_servers;

  -- Exclude localhost (id=0) or other servers — edit as needed
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    server_ids := array_remove(server_ids, 0::bigint);
  END IF;
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    UPDATE servers SET team_id = new_team WHERE id = ANY(server_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT private_key_id) INTO key_ids
  FROM applications WHERE environment_id = ANY(env_ids) AND private_key_id IS NOT NULL;
  IF key_ids IS NOT NULL AND array_length(key_ids, 1) > 0 THEN
    UPDATE private_keys SET team_id = new_team WHERE id = ANY(key_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO github_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitHub%';
  IF github_ids IS NOT NULL AND array_length(github_ids, 1) > 0 THEN
    UPDATE github_apps SET team_id = new_team WHERE id = ANY(github_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO gitlab_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitLab%';
  IF gitlab_ids IS NOT NULL AND array_length(gitlab_ids, 1) > 0 THEN
    UPDATE gitlab_apps SET team_id = new_team WHERE id = ANY(gitlab_ids) AND team_id = old_team;
  END IF;

  WITH backup_s3 AS (
    SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_postgresqls sp ON sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id = sp.id AND sp.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_redis sr ON sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id = sr.id AND sr.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mongodbs sm ON sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mysqls sm ON sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mariadbs sm ON sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
  )
  SELECT array_agg(DISTINCT s3_storage_id) INTO s3_ids FROM backup_s3 WHERE s3_storage_id IS NOT NULL;
  IF s3_ids IS NOT NULL AND array_length(s3_ids, 1) > 0 THEN
    UPDATE s3_storages SET team_id = new_team WHERE id = ANY(s3_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT t.tag_id) INTO tag_ids FROM taggables t
  WHERE (t.taggable_type LIKE '%Application' AND t.taggable_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%Service' AND t.taggable_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandalonePostgresql' AND t.taggable_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneRedis' AND t.taggable_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMongodb' AND t.taggable_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMysql' AND t.taggable_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMariadb' AND t.taggable_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)));
  IF tag_ids IS NOT NULL AND array_length(tag_ids, 1) > 0 THEN
    UPDATE tags SET team_id = new_team WHERE id = ANY(tag_ids) AND (team_id = old_team OR team_id IS NULL);
  END IF;

  UPDATE scheduled_database_backups sdb SET team_id = new_team
  WHERE sdb.team_id = old_team
    AND (
      (sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)))
    );

  UPDATE scheduled_tasks st SET team_id = new_team
  WHERE st.team_id = old_team
    AND (st.application_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids))
         OR st.service_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)));

  UPDATE shared_environment_variables SET team_id = new_team
  WHERE team_id = old_team AND (project_id = project_id_to_move OR environment_id = ANY(env_ids));

  RAISE NOTICE 'Migration of project % to team % completed.', project_id_to_move, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects WHERE id = 10;
-- SELECT id, name, team_id FROM servers WHERE team_id = 11;

COMMIT;
-- On failure: ROLLBACK;
BEGIN;

DO $$
DECLARE
  project_id_to_move bigint := 10;   -- <-- CHANGE
  old_team bigint := 0;              -- <-- CHANGE
  new_team bigint := 11;             -- <-- CHANGE
  env_ids bigint[];
  server_ids bigint[];
  key_ids bigint[];
  github_ids bigint[];
  gitlab_ids bigint[];
  s3_ids bigint[];
  tag_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO env_ids FROM environments WHERE project_id = project_id_to_move;

  UPDATE projects SET team_id = new_team WHERE id = project_id_to_move AND team_id = old_team;

  WITH dest_servers AS (
    SELECT sd.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
    UNION SELECT sw.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
    UNION SELECT sd.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
    UNION SELECT sw.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
    UNION SELECT sd.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
    UNION SELECT sw.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
  )
  SELECT array_agg(DISTINCT server_id) INTO server_ids FROM dest_servers;

  -- Exclude localhost (id=0) or other servers — edit as needed
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    server_ids := array_remove(server_ids, 0::bigint);
  END IF;
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    UPDATE servers SET team_id = new_team WHERE id = ANY(server_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT private_key_id) INTO key_ids
  FROM applications WHERE environment_id = ANY(env_ids) AND private_key_id IS NOT NULL;
  IF key_ids IS NOT NULL AND array_length(key_ids, 1) > 0 THEN
    UPDATE private_keys SET team_id = new_team WHERE id = ANY(key_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO github_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitHub%';
  IF github_ids IS NOT NULL AND array_length(github_ids, 1) > 0 THEN
    UPDATE github_apps SET team_id = new_team WHERE id = ANY(github_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO gitlab_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitLab%';
  IF gitlab_ids IS NOT NULL AND array_length(gitlab_ids, 1) > 0 THEN
    UPDATE gitlab_apps SET team_id = new_team WHERE id = ANY(gitlab_ids) AND team_id = old_team;
  END IF;

  WITH backup_s3 AS (
    SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_postgresqls sp ON sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id = sp.id AND sp.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_redis sr ON sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id = sr.id AND sr.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mongodbs sm ON sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mysqls sm ON sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mariadbs sm ON sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
  )
  SELECT array_agg(DISTINCT s3_storage_id) INTO s3_ids FROM backup_s3 WHERE s3_storage_id IS NOT NULL;
  IF s3_ids IS NOT NULL AND array_length(s3_ids, 1) > 0 THEN
    UPDATE s3_storages SET team_id = new_team WHERE id = ANY(s3_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT t.tag_id) INTO tag_ids FROM taggables t
  WHERE (t.taggable_type LIKE '%Application' AND t.taggable_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%Service' AND t.taggable_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandalonePostgresql' AND t.taggable_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneRedis' AND t.taggable_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMongodb' AND t.taggable_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMysql' AND t.taggable_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMariadb' AND t.taggable_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)));
  IF tag_ids IS NOT NULL AND array_length(tag_ids, 1) > 0 THEN
    UPDATE tags SET team_id = new_team WHERE id = ANY(tag_ids) AND (team_id = old_team OR team_id IS NULL);
  END IF;

  UPDATE scheduled_database_backups sdb SET team_id = new_team
  WHERE sdb.team_id = old_team
    AND (
      (sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)))
    );

  UPDATE scheduled_tasks st SET team_id = new_team
  WHERE st.team_id = old_team
    AND (st.application_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids))
         OR st.service_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)));

  UPDATE shared_environment_variables SET team_id = new_team
  WHERE team_id = old_team AND (project_id = project_id_to_move OR environment_id = ANY(env_ids));

  RAISE NOTICE 'Migration of project % to team % completed.', project_id_to_move, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects WHERE id = 10;
-- SELECT id, name, team_id FROM servers WHERE team_id = 11;

COMMIT;
-- On failure: ROLLBACK;
BEGIN;

DO $$
DECLARE
  project_id_to_move bigint := 10;   -- <-- CHANGE
  old_team bigint := 0;              -- <-- CHANGE
  new_team bigint := 11;             -- <-- CHANGE
  env_ids bigint[];
  server_ids bigint[];
  key_ids bigint[];
  github_ids bigint[];
  gitlab_ids bigint[];
  s3_ids bigint[];
  tag_ids bigint[];
BEGIN
  SELECT array_agg(id) INTO env_ids FROM environments WHERE project_id = project_id_to_move;

  UPDATE projects SET team_id = new_team WHERE id = project_id_to_move AND team_id = old_team;

  WITH dest_servers AS (
    SELECT sd.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON a.destination_type LIKE '%StandaloneDocker' AND a.destination_id = sd.id
    UNION SELECT sw.server_id FROM applications a
    JOIN environments e ON e.id = a.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON a.destination_type LIKE '%SwarmDocker' AND a.destination_id = sw.id
    UNION SELECT sd.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON srv.destination_type LIKE '%StandaloneDocker' AND srv.destination_id = sd.id
    UNION SELECT sw.server_id FROM services srv
    JOIN environments e ON e.id = srv.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON srv.destination_type LIKE '%SwarmDocker' AND srv.destination_id = sw.id
    UNION SELECT sd.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN standalone_dockers sd ON sp.destination_type LIKE '%StandaloneDocker' AND sp.destination_id = sd.id
    UNION SELECT sw.server_id FROM standalone_postgresqls sp
    JOIN environments e ON e.id = sp.environment_id AND e.project_id = project_id_to_move
    JOIN swarm_dockers sw ON sp.destination_type LIKE '%SwarmDocker' AND sp.destination_id = sw.id
  )
  SELECT array_agg(DISTINCT server_id) INTO server_ids FROM dest_servers;

  -- Exclude localhost (id=0) or other servers — edit as needed
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    server_ids := array_remove(server_ids, 0::bigint);
  END IF;
  IF server_ids IS NOT NULL AND array_length(server_ids, 1) > 0 THEN
    UPDATE servers SET team_id = new_team WHERE id = ANY(server_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT private_key_id) INTO key_ids
  FROM applications WHERE environment_id = ANY(env_ids) AND private_key_id IS NOT NULL;
  IF key_ids IS NOT NULL AND array_length(key_ids, 1) > 0 THEN
    UPDATE private_keys SET team_id = new_team WHERE id = ANY(key_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO github_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitHub%';
  IF github_ids IS NOT NULL AND array_length(github_ids, 1) > 0 THEN
    UPDATE github_apps SET team_id = new_team WHERE id = ANY(github_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT a.source_id) INTO gitlab_ids
  FROM applications a WHERE a.environment_id = ANY(env_ids) AND a.source_type IS NOT NULL AND a.source_type LIKE '%GitLab%';
  IF gitlab_ids IS NOT NULL AND array_length(gitlab_ids, 1) > 0 THEN
    UPDATE gitlab_apps SET team_id = new_team WHERE id = ANY(gitlab_ids) AND team_id = old_team;
  END IF;

  WITH backup_s3 AS (
    SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_postgresqls sp ON sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id = sp.id AND sp.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_redis sr ON sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id = sr.id AND sr.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mongodbs sm ON sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mysqls sm ON sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
    UNION SELECT sdb.s3_storage_id FROM scheduled_database_backups sdb
    JOIN standalone_mariadbs sm ON sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id = sm.id AND sm.environment_id = ANY(env_ids) WHERE sdb.s3_storage_id IS NOT NULL
  )
  SELECT array_agg(DISTINCT s3_storage_id) INTO s3_ids FROM backup_s3 WHERE s3_storage_id IS NOT NULL;
  IF s3_ids IS NOT NULL AND array_length(s3_ids, 1) > 0 THEN
    UPDATE s3_storages SET team_id = new_team WHERE id = ANY(s3_ids) AND team_id = old_team;
  END IF;

  SELECT array_agg(DISTINCT t.tag_id) INTO tag_ids FROM taggables t
  WHERE (t.taggable_type LIKE '%Application' AND t.taggable_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%Service' AND t.taggable_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandalonePostgresql' AND t.taggable_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneRedis' AND t.taggable_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMongodb' AND t.taggable_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMysql' AND t.taggable_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
     OR (t.taggable_type LIKE '%StandaloneMariadb' AND t.taggable_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)));
  IF tag_ids IS NOT NULL AND array_length(tag_ids, 1) > 0 THEN
    UPDATE tags SET team_id = new_team WHERE id = ANY(tag_ids) AND (team_id = old_team OR team_id IS NULL);
  END IF;

  UPDATE scheduled_database_backups sdb SET team_id = new_team
  WHERE sdb.team_id = old_team
    AND (
      (sdb.database_type LIKE '%StandalonePostgresql' AND sdb.database_id IN (SELECT id FROM standalone_postgresqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneRedis' AND sdb.database_id IN (SELECT id FROM standalone_redis WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMongodb' AND sdb.database_id IN (SELECT id FROM standalone_mongodbs WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMysql' AND sdb.database_id IN (SELECT id FROM standalone_mysqls WHERE environment_id = ANY(env_ids)))
      OR (sdb.database_type LIKE '%StandaloneMariadb' AND sdb.database_id IN (SELECT id FROM standalone_mariadbs WHERE environment_id = ANY(env_ids)))
    );

  UPDATE scheduled_tasks st SET team_id = new_team
  WHERE st.team_id = old_team
    AND (st.application_id IN (SELECT id FROM applications WHERE environment_id = ANY(env_ids))
         OR st.service_id IN (SELECT id FROM services WHERE environment_id = ANY(env_ids)));

  UPDATE shared_environment_variables SET team_id = new_team
  WHERE team_id = old_team AND (project_id = project_id_to_move OR environment_id = ANY(env_ids));

  RAISE NOTICE 'Migration of project % to team % completed.', project_id_to_move, new_team;
END $$;

-- Verify before COMMIT:
-- SELECT id, name, team_id FROM projects WHERE id = 10;
-- SELECT id, name, team_id FROM servers WHERE team_id = 11;

COMMIT;
-- On failure: ROLLBACK;

5.4 Pre-migration state check

Overview of resource counts per team (before migrating an entire team or before a dry-run):

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

SELECT 'projects' AS table_name, team_id, count(*) FROM projects GROUP BY team_id
UNION ALL
SELECT 'servers', team_id, count(*) FROM servers GROUP BY team_id
UNION ALL
SELECT 'private_keys', team_id, count(*) FROM private_keys GROUP BY team_id
UNION ALL
SELECT 'github_apps', team_id, count(*) FROM github_apps GROUP BY team_id
UNION ALL
SELECT 's3_storages', team_id, count(*) FROM s3_storages GROUP BY team_id
UNION ALL
SELECT 'tags', team_id, count(*) FROM tags GROUP BY team_id
ORDER BY table_name,

6. Reverting changes and exceptions

6.1 Revert localhost server to team 0

If the “localhost” server (id=0) was moved to another team and should remain in team 0:

UPDATE servers
SET team_id = 0
WHERE id = 0 AND team_id = 11;

SELECT id, name, team_id FROM servers WHERE id = 0

UPDATE servers
SET team_id = 0
WHERE id = 0 AND team_id = 11;

SELECT id, name, team_id FROM servers WHERE id = 0

UPDATE servers
SET team_id = 0
WHERE id = 0 AND team_id = 11;

SELECT id, name, team_id FROM servers WHERE id = 0

No other tables store the server’s team membership — only servers.team_id. Relations (applications, services, standalone_dockers) use server_id; after reverting the team, the server continues to work; only its visibility in the Coolify UI changes.

6.2 Reverting an entire migration

If the migration ran in a single transaction and COMMIT was not executed, run ROLLBACK.

If COMMIT was already run, restoring the previous state requires a database restore from backup or manual UPDATEs in the opposite direction (using saved dry-run results and backup).

7. UI verification and cache

  • After migration, the project and servers have the target team_id in the database. In Coolify, switch the team view to the target team (e.g. “A Team”) — the project and servers are visible only there.

  • If the UI still shows the old state: clear cache (e.g. php artisan cache:clear, php artisan config:clear in the Coolify container) and restart the Coolify container; hard-refresh the browser (Ctrl+Shift+R).

  • Team access: the user must be in team_user for the target team. Check with: SELECT * FROM team_user WHERE team_id = 11;. If needed, add a row (e.g. INSERT INTO team_user (team_id, user_id, role) VALUES (11, <user_id>, 'member');).

Start your next chapter with the right partner

Start your next chapter with the right partner

Start your next chapter with the right partner