diff --git a/.circleci/config.yml b/.circleci/config.yml
index 239f17401..ac2b3bb09 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,10 +1,10 @@
 version: 2.1 # CircleCI version
 orbs:
-  sonarcloud: sonarsource/sonarcloud@1.1.1
+  sonarcloud: sonarsource/sonarcloud@2.0.0
 jobs:
   build:
     machine: #Linux machine instead of docker environment
-      image: ubuntu-2004:202111-01
+      image: ubuntu-2204:current
       docker_layer_caching: true
     working_directory: ~/user # Default working directory
     steps:
@@ -52,4 +52,3 @@ workflows:
             tags:
               only:
                 - develop
-#test commit
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..389a4d15c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,184 @@
+# AGENTS.md
+
+## Purpose
+
+This repository contains the ELEVATE User Service (`com.shikshalokam.mentoring.userservice`), a Node.js/Express API for user, organization, role/permission, invite, and tenant workflows.
+
+## Repository Layout
+
+-   `src/` - Main service codebase (this is where almost all work happens).
+-   `src/app.js` - Service entrypoint.
+-   `src/routes/index.js` - Dynamic API router and global error handling.
+-   `src/controllers/v1/` - Versioned controller endpoints.
+-   `src/services/` - Business logic layer.
+-   `src/database/` - Sequelize models, queries, migrations, seeders.
+-   `src/middlewares/` - Express middlewares: `authenticator.js` (JWT/auth), `pagination.js`, `validator.js`. All routes pass through these; touch with care.
+-   `src/validators/v1/` - Request validation schemas (one per controller). Every new endpoint should have a matching validator here.
+-   `src/generics/` - Shared infrastructure utilities: `utils.js` (large general helpers), `materializedViews.js`, `kafka-communication.js`, `redis-communication.js`, `RollbackStack.js`. Check here before writing new utility code.
+-   `src/dtos/` - Data transfer objects for user, org, tenant, and events. Update when adding or changing fields in API responses.
+-   `src/constants/` - App-wide constants: `common.js`, `blacklistConfig.js`, endpoint definitions. Don't define new constants inline in services/controllers.
+-   `src/locales/` - i18n string files (`en.json`, `hi.json`). All user-facing messages must be added here, not hardcoded.
+-   `src/configs/` - Kafka, Redis, cache, cloud-storage, queue worker setup.
+-   `src/scripts/` - Operational scripts and data migration utilities.
+-   `src/health-checks/` - Health check endpoints and config.
+-   `dev-ops/` - Dependency docker compose and reporting helpers.
+-   `README.md` - Full setup docs and dependency installation notes.
+
+## Stack and Runtime
+
+-   Node.js 20 (recommended in `README.md`)
+-   Express 4
+-   PostgreSQL (with Citus in some deployments)
+-   Sequelize + `sequelize-cli`
+-   Redis
+-   Kafka (`kafkajs`)
+-   BullMQ worker (invites and bulk-user workflows)
+
+## Critical Context
+
+-   Run service commands from `src/`, not repo root.
+-   Repo root has a minimal `package.json` used for tooling; the real app package is `src/package.json`.
+-   App startup validates env vars via `src/envVariables.js`; missing required vars will stop boot.
+-   Route format is dynamic:
+    -   `${APPLICATION_BASE_URL}/:version/:controller/:method`
+    -   `${APPLICATION_BASE_URL}/:version/:controller/:file/:method`
+
+## Local Setup (Fast Path)
+
+1. `cd src`
+2. `cp .env.sample .env`
+3. Fill required env values (see `src/envVariables.js` and `src/.env.sample`)
+4. `npm install`
+5. `npm run db:migrate`
+6. `npm run db:seed:all` (optional but common for local)
+7. `npm start`
+
+Default local app URL is typically `http://localhost:3001` (or `APPLICATION_PORT` from `.env`).
+
+## Core Commands
+
+From `src/`:
+
+-   `npm start` - run in development with nodemon
+-   `npm run prod` - production mode
+-   `npm run qa` / `npm run stage` - environment-specific starts
+-   `npm run db:init` - create DB + migrate
+-   `npm run db:migrate` - run migrations
+-   `npm run db:seed:all` - run all seeders
+    > **Note:** Tests (unit and integration) are currently broken and should not be run. Omit any test steps until further notice.
+
+## Important Env Groups
+
+See full list in `src/envVariables.js`; key groups:
+
+-   App/Auth: `APPLICATION_*`, `ACCESS_TOKEN_*`, `REFRESH_TOKEN_*`, `API_DOC_URL`
+-   Database: `DEV_DATABASE_URL`, `TEST_DATABASE_URL`, `DATABASE_URL`, `DB_POOL_*`
+-   Kafka: `KAFKA_URL`, `KAFKA_GROUP_ID`, event topic toggles and topic names
+-   Redis/Cache: `REDIS_HOST`, `INTERNAL_CACHE_EXP_TIME`
+-   Storage: `CLOUD_STORAGE_PROVIDER`, `CLOUD_STORAGE_*`, `PUBLIC_ASSET_BUCKETNAME`
+-   Integrations: `MENTORING_SERVICE_URL`, `ENTITY_MANAGEMENT_SERVICE_BASE_URL`, scheduler and notification vars
+
+## Health Endpoints
+
+-   `GET /health`
+-   `GET /healthCheckStatus`
+
+Health config lives in `src/health-checks/health.config.js` and currently checks kafka, redis, postgres, plus dependent services.
+
+## Operational Scripts
+
+From `src/`:
+
+-   `npm run migrate:tenant-org-data` - tenant/org data move script
+-   `npm run check:user-in-account-search -- --auth-token=<token> ...` - paginated account search checker
+-   `node scripts/insertDefaultOrg.js` - bootstrap default org
+-   `node scripts/encryptDecryptEmails.js encrypt|decrypt`
+
+More script notes: `src/scripts/readme.md`.
+
+## Code Style and Tooling
+
+-   ESLint rules: `src/.eslintrc.json` (tabs, single quotes, no semicolons)
+-   Prettier: `.prettierrc.json` at repo root
+-   Husky pre-commit runs `lint-staged` from `src/`
+
+## Module Aliases
+
+Defined in `src/package.json` under `_moduleAliases`. Always use these instead of relative paths:
+
+| Alias            | Resolves to          |
+| ---------------- | -------------------- |
+| `@root`          | `src/`               |
+| `@configs`       | `src/configs/`       |
+| `@constants`     | `src/constants/`     |
+| `@controllers`   | `src/controllers/`   |
+| `@database`      | `src/database/`      |
+| `@generics`      | `src/generics/`      |
+| `@health-checks` | `src/health-checks/` |
+| `@middlewares`   | `src/middlewares/`   |
+| `@routes`        | `src/routes/`        |
+| `@services`      | `src/services/`      |
+| `@validators`    | `src/validators/`    |
+| `@utils`         | `src/utils/`         |
+| `@helpers`       | `src/helpers/`       |
+| `@scripts`       | `src/scripts/`       |
+| `@dtos`          | `src/dtos/`          |
+| `@public`        | `src/public/`        |
+
+## Implementation Guardrails
+
+-   Keep controller-service-query layering intact.
+-   Preserve response shape used by router/error middleware (`statusCode`, `responseCode`, `message`, `result`, `meta`).
+-   For DB schema changes, add Sequelize migrations in `src/database/migrations/`.
+-   Prefer updating existing query/service modules instead of embedding raw SQL in controllers.
+-   If changing env requirements, update both `src/envVariables.js` and `src/.env.sample`.
+-   All user-facing strings must be added to `src/locales/en.json` (and `hi.json` if translatable). Never hardcode message strings in services or controllers.
+
+## Pull Request Instructions
+
+1. Keep PRs focused on one logical change (avoid mixing refactor + feature + migration unless required).
+2. Rebase/sync with latest target branch before opening PR.
+3. Run lint validation from `src/`:
+    - `npx eslint .` (if lint-sensitive files changed)
+        > **Note:** Unit and integration tests are currently broken — skip test steps.
+4. Include migration/rollback notes in PR description when touching `src/database/migrations/`.
+5. If env/config changes are introduced, update:
+    - `src/.env.sample`
+    - `src/envVariables.js`
+    - relevant README/notes
+6. PR description should include:
+    - What changed
+    - Why it changed
+    - Risk/impact
+    - Test evidence (commands + summary, if applicable — tests currently broken)
+    - API contract changes (if any)
+
+## Commit Message Format
+
+Use Conventional Commit style:
+
+-   `<type>(<scope>): <subject>`
+
+Example:
+
+-   `refactor(organization): optimize feature access logic for role mappings`
+
+Allowed `type` values:
+
+-   `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `chore`, `build`, `ci`
+
+Scope guidance:
+
+-   Use module/domain names such as `organization`, `user`, `tenant`, `roles`, `scripts`, `migrations`, `health-checks`, `configs`.
+
+Subject guidance:
+
+-   Use imperative mood and keep it concise.
+-   Do not end subject with a period.
+
+## Useful References
+
+-   Main setup and infra guidance: `README.md`
+-   Sequelize path mapping: `src/.sequelizerc`
+-   Citus distribution SQL helper: `src/distributionColumns.sql`
+-   Health check guide: `src/health-checks/README.md`
diff --git a/src/constants/common.js b/src/constants/common.js
index 03f4eccba..7a0edcf72 100644
--- a/src/constants/common.js
+++ b/src/constants/common.js
@@ -116,4 +116,20 @@ module.exports = {
 	SIGNEDUP_STATUS: 'SIGNEDUP',
 	SEQUELIZE_UNIQUE_CONSTRAINT_ERROR: 'SequelizeUniqueConstraintError',
 	SEQUELIZE_UNIQUE_CONSTRAINT_ERROR_CODE: 'ER_DUP_ENTRY',
+	DEFAULT_TENANT_CONFIGURATION: {
+		allowed_auth_mode: process.env.DEFAULT_ALLOWED_AUTH_MODES.split(','),
+		auto_register: process.env.DEFAULT_AUTO_REGISTER === 'true',
+	},
+	AUTH_MODES: {
+		OTP: 'otp',
+		PASSWORD: 'password',
+	},
+	OTP_PURPOSES: {
+		SIGNUP: 'signup',
+		LOGIN: 'login',
+	},
+	EMAIL: 'email',
+	PHONE: 'phone',
+	USER_NAME: 'username',
+	USER: 'User',
 }
diff --git a/src/database/migrations/20260618090000-add-configuration-to-tenants.js b/src/database/migrations/20260618090000-add-configuration-to-tenants.js
new file mode 100644
index 000000000..682d3b074
--- /dev/null
+++ b/src/database/migrations/20260618090000-add-configuration-to-tenants.js
@@ -0,0 +1,28 @@
+'use strict'
+
+const common = require('@constants/common')
+
+module.exports = {
+	up: async (queryInterface, Sequelize) => {
+		await queryInterface.addColumn('tenants', 'configuration', {
+			type: Sequelize.JSONB,
+			allowNull: false,
+			defaultValue: common.DEFAULT_TENANT_CONFIGURATION,
+		})
+
+		await queryInterface.sequelize.query(
+			`UPDATE tenants
+			 SET configuration = :configuration
+			 WHERE configuration IS NULL`,
+			{
+				replacements: {
+					configuration: JSON.stringify(common.DEFAULT_TENANT_CONFIGURATION),
+				},
+			}
+		)
+	},
+
+	down: async (queryInterface) => {
+		await queryInterface.removeColumn('tenants', 'configuration')
+	},
+}
diff --git a/src/database/migrations/20260621090000-make-user-name-and-password-nullable.js b/src/database/migrations/20260621090000-make-user-name-and-password-nullable.js
new file mode 100644
index 000000000..80258d412
--- /dev/null
+++ b/src/database/migrations/20260621090000-make-user-name-and-password-nullable.js
@@ -0,0 +1,44 @@
+'use strict'
+
+module.exports = {
+	up: async (queryInterface, Sequelize) => {
+		await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users;')
+
+		await queryInterface.changeColumn('users', 'name', {
+			type: Sequelize.STRING,
+			allowNull: true,
+		})
+
+		await queryInterface.changeColumn('users', 'password', {
+			type: Sequelize.STRING,
+			allowNull: true,
+		})
+
+		await queryInterface.changeColumn('users_credentials', 'password', {
+			type: Sequelize.STRING,
+			allowNull: true,
+		})
+	},
+
+	down: async (queryInterface, Sequelize) => {
+		await queryInterface.sequelize.query('DROP MATERIALIZED VIEW IF EXISTS m_users;')
+
+		await queryInterface.sequelize.query(`UPDATE users SET name = '' WHERE name IS NULL`)
+		await queryInterface.sequelize.query(`UPDATE users SET password = '' WHERE password IS NULL`)
+		await queryInterface.sequelize.query(`UPDATE users_credentials SET password = '' WHERE password IS NULL`)
+		await queryInterface.changeColumn('users', 'name', {
+			type: Sequelize.STRING,
+			allowNull: false,
+		})
+
+		await queryInterface.changeColumn('users', 'password', {
+			type: Sequelize.STRING,
+			allowNull: false,
+		})
+
+		await queryInterface.changeColumn('users_credentials', 'password', {
+			type: Sequelize.STRING,
+			allowNull: false,
+		})
+	},
+}
diff --git a/src/database/migrations/20260623124000-add-name-entity-type.js b/src/database/migrations/20260623124000-add-name-entity-type.js
new file mode 100644
index 000000000..8f44fe234
--- /dev/null
+++ b/src/database/migrations/20260623124000-add-name-entity-type.js
@@ -0,0 +1,71 @@
+'use strict'
+
+module.exports = {
+	async up(queryInterface, Sequelize) {
+		const transaction = await queryInterface.sequelize.transaction()
+		try {
+			const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE
+
+			const defaultOrgsPerTenant = await queryInterface.sequelize.query(
+				'SELECT id, code, tenant_code FROM organizations WHERE code = :defaultOrgCode',
+				{
+					replacements: { defaultOrgCode },
+					type: queryInterface.sequelize.QueryTypes.SELECT,
+					transaction,
+				}
+			)
+
+			if (!defaultOrgsPerTenant.length) {
+				console.warn('No default organizations found across tenants. Skipping migration.')
+				await transaction.commit()
+				return
+			}
+
+			const now = new Date()
+			const entityTypesToInsert = defaultOrgsPerTenant.map((org) => ({
+				value: 'name',
+				label: 'Name',
+				status: 'ACTIVE',
+				created_by: null,
+				updated_by: null,
+				allow_filtering: false,
+				data_type: 'STRING',
+				organization_id: org.id,
+				organization_code: org.code,
+				tenant_code: org.tenant_code,
+				parent_id: null,
+				has_entities: false,
+				allow_custom_entities: false,
+				model_names: ['User'],
+				created_at: now,
+				updated_at: now,
+			}))
+
+			await queryInterface.bulkInsert('entity_types', entityTypesToInsert, { transaction })
+
+			await transaction.commit()
+		} catch (error) {
+			await transaction.rollback()
+			console.error('Migration up failed:', error)
+			throw error
+		}
+	},
+
+	async down(queryInterface, Sequelize) {
+		const transaction = await queryInterface.sequelize.transaction()
+		try {
+			await queryInterface.bulkDelete(
+				'entity_types',
+				{
+					value: 'name',
+				},
+				{ transaction }
+			)
+			await transaction.commit()
+		} catch (error) {
+			await transaction.rollback()
+			console.error('Migration down failed:', error)
+			throw error
+		}
+	},
+}
diff --git a/src/database/models/Tenant.js b/src/database/models/Tenant.js
index 8b6d8eebc..d03a422fc 100644
--- a/src/database/models/Tenant.js
+++ b/src/database/models/Tenant.js
@@ -1,4 +1,6 @@
 'use strict'
+const common = require('@constants/common')
+
 module.exports = (sequelize, DataTypes) => {
 	const Tenant = sequelize.define(
 		'Tenant',
@@ -28,6 +30,11 @@ module.exports = (sequelize, DataTypes) => {
 				type: DataTypes.JSONB,
 				allowNull: true,
 			},
+			configuration: {
+				type: DataTypes.JSONB,
+				allowNull: false,
+				defaultValue: common.DEFAULT_TENANT_CONFIGURATION,
+			},
 			meta: {
 				type: DataTypes.JSON,
 				allowNull: true,
diff --git a/src/database/models/users.js b/src/database/models/users.js
index a4d612680..71e12c533 100644
--- a/src/database/models/users.js
+++ b/src/database/models/users.js
@@ -20,11 +20,11 @@ module.exports = (sequelize, DataTypes) => {
 			},
 			password: {
 				type: DataTypes.STRING,
-				allowNull: false,
+				allowNull: true,
 			},
 			name: {
 				type: DataTypes.STRING,
-				allowNull: false,
+				allowNull: true,
 			},
 			username: {
 				type: DataTypes.STRING,
diff --git a/src/dtos/tenantDTO.js b/src/dtos/tenantDTO.js
index f9b85e748..c5a381a76 100644
--- a/src/dtos/tenantDTO.js
+++ b/src/dtos/tenantDTO.js
@@ -7,6 +7,7 @@ class TenantResponseDTO {
 			description: input.tenant.description,
 			logo: input.tenant.logo,
 			theming: input.tenant.theming,
+			configuration: input.tenant.configuration,
 			meta: input.tenant.meta,
 
 			...(input.organization && {
diff --git a/src/envVariables.js b/src/envVariables.js
index 2fca2daf8..52f25a9cd 100644
--- a/src/envVariables.js
+++ b/src/envVariables.js
@@ -516,6 +516,16 @@ let enviromentVariables = {
 		optional: true,
 		default: 'UserService',
 	},
+	DEFAULT_ALLOWED_AUTH_MODES: {
+		message: 'Required DEFAULT_ALLOWED_AUTH_MODES',
+		optional: true,
+		default: ['otp', 'password'],
+	},
+	DEFAULT_AUTO_REGISTER: {
+		message: 'Required DEFAULT_AUTO_REGISTER',
+		optional: true,
+		default: true,
+	},
 }
 let success = true
 
diff --git a/src/generics/utils.js b/src/generics/utils.js
index e383ae26e..ec06c5bc6 100644
--- a/src/generics/utils.js
+++ b/src/generics/utils.js
@@ -1135,6 +1135,13 @@ function extractDelta(oldData = {}, newData = {}, updateData) {
 	return delta
 }
 
+function checkIdentifierType(identifier) {
+	if (/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(identifier)) return common.EMAIL
+	if (/^\+?[1-9]\d{1,14}$/.test(identifier)) return common.PHONE
+	if (/^[a-zA-Z0-9_-]{3,30}$/.test(identifier)) return common.USER_NAME
+	return null
+}
+
 module.exports = {
 	generateToken,
 	hashPassword,
@@ -1183,4 +1190,5 @@ module.exports = {
 	parseMetaData,
 	extractUpdatedValues,
 	extractDelta,
+	checkIdentifierType,
 }
diff --git a/src/locales/en.json b/src/locales/en.json
index 5a864343b..f241fd54c 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -32,6 +32,11 @@
 	"REGISTRATION_OTP_SENT_SUCCESSFULLY": "OTP has been sent to your provided email or phone number. Please enter the OTP to complete your registration.",
 	"PASSWORD_RESET_SUCCESSFULLY": "Password reset successfully.",
 	"OTP_INVALID": "The OTP you entered is wrong.",
+	"OTP_REQUIRED": "OTP is required.",
+	"AUTH_MODE_NOT_ALLOWED": "This authentication mode is not enabled for the tenant.",
+	"AUTH_CREDENTIAL_REQUIRED": "Password or OTP is required.",
+	"PASSWORD_REQUIRED": "Password is required.",
+	"USER_CONTACT_NOT_AVAILABLE": "No email or phone number is available for this account. Please contact your administrator.",
 	"RESET_OTP_INVALID": "Please enter the correct OTP.",
 	"ADMIN_USER_ALREADY_EXISTS": "Admin User already exists.",
 	"NOT_AN_ADMIN": "Not an admin",
@@ -151,7 +156,7 @@
 	"TENANT_LIST_FETCHED": "Tenant list fetched successfully.",
 	"TENANT_DOMAIN_NOT_FOUND_PING_ADMIN": "Tenant domain not found. Please contact your administrator for assistance.",
 	"TENANT_NOT_FOUND_PING_ADMIN": "Tenant not found. Please contact your administrator for assistance.",
-	"EMAIL_OR_PHONE_REQUIRED": "Either an email address or phone number is required.",
+	"EMAIL_OR_PHONE_REQUIRED": "Either an email address, phone number, or username is required.",
 	"INVALID_ORG_REGISTRATION_CODE": "The organization registration code you entered is invalid. Please check and try again.",
 	"IDENTIFIER_OR_PASSWORD_INVALID": "Invalid identifier or password. Please check your login credentials.",
 	"IDENTIFIER_REQUIRED": "An identifier (email or phone) is required to proceed.",
diff --git a/src/locales/hi.json b/src/locales/hi.json
index 6d77d32d4..11d7d2c04 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -32,6 +32,11 @@
 	"REGISTRATION_OTP_SENT_SUCCESSFULLY": "आपके पंजीकृत ईमेल आईडी पर ओटीपी भेजा गया है। कृपया पंजीकरण प्रक्रिया को पूरा करने के लिए ओटीपी दर्ज करें।",
 	"PASSWORD_RESET_SUCCESSFULLY": "पासवर्ड सफलतापूर्वक बदला गया।",
 	"OTP_INVALID": "आपके द्वारा दर्ज किया हुआ OTP गलत है।",
+	"OTP_REQUIRED": "OTP आवश्यक है।",
+	"AUTH_MODE_NOT_ALLOWED": "इस टेनेंट के लिए यह authentication mode सक्षम नहीं है।",
+	"AUTH_CREDENTIAL_REQUIRED": "पासवर्ड या OTP आवश्यक है।",
+	"PASSWORD_REQUIRED": "पासवर्ड आवश्यक है।",
+	"USER_CONTACT_NOT_AVAILABLE": "इस खाते के लिए कोई ईमेल या फोन नंबर उपलब्ध नहीं है। कृपया अपने व्यवस्थापक से संपर्क करें।",
 	"RESET_OTP_INVALID": "कृपया सही ओटीपी दर्ज करें।",
 	"ADMIN_USER_ALREADY_EXISTS": "Admin User already exists.",
 	"NOT_AN_ADMIN": "Not an admin",
diff --git a/src/package.json b/src/package.json
index e541c3a41..984263e71 100644
--- a/src/package.json
+++ b/src/package.json
@@ -45,7 +45,7 @@
 		"elevate-encryption": "^1.0.1",
 		"elevate-logger": "^3.1.0",
 		"elevate-node-cache": "^1.0.6",
-		"elevate-services-health-check": "^0.0.9",
+		"elevate-services-health-check": "^0.0.10",
 		"email-validator": "^2.0.4",
 		"express": "^4.17.1",
 		"express-fileupload": "^1.2.1",
diff --git a/src/scripts/migrateTenantOrgData.js b/src/scripts/migrateTenantOrgData.js
new file mode 100644
index 000000000..2824d5fab
--- /dev/null
+++ b/src/scripts/migrateTenantOrgData.js
@@ -0,0 +1,2438 @@
+'use strict'
+
+require('module-alias/register')
+
+const path = require('path')
+const minimist = require('minimist')
+const axios = require('axios')
+const { Sequelize, QueryTypes, Transaction } = require('sequelize')
+
+require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+
+const DEFAULTS = {
+	roleResolution: 'strict-id',
+	strictIdRebase: 'if-target-tenant-empty',
+	deleteMode: 'soft',
+	deleteScope: 'users-only',
+	sessionMode: 'invalidate',
+	lockStrategy: 'skip',
+	dryRun: false,
+}
+
+const VALID_OPTIONS = {
+	roleResolution: ['strict-id', 'map-by-title'],
+	strictIdRebase: ['if-target-tenant-empty', 'never'],
+	deleteMode: ['soft', 'hard', 'none'],
+	deleteScope: ['users-only', 'all-copied'],
+	sessionMode: ['invalidate', 'migrate'],
+	lockStrategy: ['skip', 'advisory-only', 'advisory-table-lock'],
+}
+
+const ORG_TABLES = ['forms', 'entity_types', 'entities', 'notification_templates']
+const USER_TABLES = ['users', 'user_organizations', 'user_organization_roles']
+const EXTERNAL_FETCH_CHUNK_SIZE = 200
+const EXTERNAL_FETCH_MAX_PARALLEL = 3
+const EXTERNAL_FETCH_PAGE_LIMIT = 100
+
+function log(level, event, data = {}) {
+	console.log(
+		JSON.stringify({
+			ts: new Date().toISOString(),
+			level,
+			event,
+			...data,
+		})
+	)
+}
+
+function parseBoolean(value, defaultValue = false) {
+	if (value === undefined || value === null || value === '') {
+		return defaultValue
+	}
+	if (typeof value === 'boolean') {
+		return value
+	}
+	const normalized = String(value).trim().toLowerCase()
+	if (['true', '1', 'yes', 'y'].includes(normalized)) {
+		return true
+	}
+	if (['false', '0', 'no', 'n'].includes(normalized)) {
+		return false
+	}
+	throw new Error(`Invalid boolean value: ${value}`)
+}
+
+function getOption(cliValue, envValue, fallback) {
+	if (cliValue !== undefined && cliValue !== null && String(cliValue).trim() !== '') {
+		return cliValue
+	}
+	if (envValue !== undefined && envValue !== null && String(envValue).trim() !== '') {
+		return envValue
+	}
+	return fallback
+}
+
+function getNodeEnv() {
+	return process.env.NODE_ENV || process.env.APPLICATION_ENV || 'development'
+}
+
+function getDatabaseUrl(nodeEnv) {
+	switch (nodeEnv) {
+		case 'production':
+			return process.env.PROD_DATABASE_URL || process.env.DATABASE_URL
+		case 'test':
+			return process.env.TEST_DATABASE_URL || process.env.DATABASE_URL
+		default:
+			return process.env.DEV_DATABASE_URL || process.env.DATABASE_URL
+	}
+}
+
+function printUsage() {
+	console.log(`
+Usage:
+  node scripts/migrateTenantOrgData.js --current-tenant-code=<sourceTenant> --current-org-code=<orgCode> --new-tenant-code=<targetTenant> [options]
+
+Required:
+  --current-tenant-code
+  --current-org-code
+  --new-tenant-code
+
+Optional:
+  --role-resolution=strict-id|map-by-title             (default: strict-id)
+  --strict-id-rebase=if-target-tenant-empty|never     (default: if-target-tenant-empty)
+  --delete-mode=soft|hard|none                        (default: soft)
+  --delete-scope=users-only|all-copied                (default: users-only)
+  --session-mode=invalidate|migrate                   (default: invalidate)
+  --lock-strategy=skip|advisory-only|advisory-table-lock (default: skip)
+  --dry-run=true|false                                (default: false)
+  --help
+`)
+}
+
+function normalizeAndValidateOptions(rawOptions) {
+	const normalized = {
+		currentTenantCode: String(rawOptions.currentTenantCode || '').trim(),
+		currentOrgCode: String(rawOptions.currentOrgCode || '').trim(),
+		newTenantCode: String(rawOptions.newTenantCode || '').trim(),
+		roleResolution: String(rawOptions.roleResolution || DEFAULTS.roleResolution)
+			.trim()
+			.toLowerCase(),
+		strictIdRebase: String(rawOptions.strictIdRebase || DEFAULTS.strictIdRebase)
+			.trim()
+			.toLowerCase(),
+		deleteMode: String(rawOptions.deleteMode || DEFAULTS.deleteMode)
+			.trim()
+			.toLowerCase(),
+		deleteScope: String(rawOptions.deleteScope || DEFAULTS.deleteScope)
+			.trim()
+			.toLowerCase(),
+		sessionMode: String(rawOptions.sessionMode || DEFAULTS.sessionMode)
+			.trim()
+			.toLowerCase(),
+		lockStrategy: String(rawOptions.lockStrategy || DEFAULTS.lockStrategy)
+			.trim()
+			.toLowerCase(),
+		dryRun: parseBoolean(rawOptions.dryRun, DEFAULTS.dryRun),
+	}
+
+	if (!normalized.currentTenantCode || !normalized.currentOrgCode || !normalized.newTenantCode) {
+		throw new Error('Missing required args: current-tenant-code, current-org-code, new-tenant-code')
+	}
+
+	if (normalized.currentTenantCode === normalized.newTenantCode) {
+		throw new Error('current-tenant-code and new-tenant-code must be different')
+	}
+
+	Object.keys(VALID_OPTIONS).forEach((key) => {
+		if (!VALID_OPTIONS[key].includes(normalized[key])) {
+			throw new Error(`Invalid ${key}: ${normalized[key]}. Allowed: ${VALID_OPTIONS[key].join(', ')}`)
+		}
+	})
+
+	return normalized
+}
+
+async function querySelect(sequelize, sql, bind, transaction) {
+	return sequelize.query(sql, {
+		type: QueryTypes.SELECT,
+		bind,
+		transaction,
+	})
+}
+
+async function queryRaw(sequelize, sql, bind, transaction) {
+	return sequelize.query(sql, {
+		bind,
+		transaction,
+	})
+}
+
+function assertOrThrow(condition, message, details = {}) {
+	if (!condition) {
+		const error = new Error(message)
+		error.details = details
+		throw error
+	}
+}
+
+function mapRoleArray(roles, roleMap) {
+	if (!Array.isArray(roles) || roles.length === 0) {
+		return []
+	}
+	return roles.map((roleId) => {
+		const mapped = roleMap.get(Number(roleId))
+		if (!mapped) {
+			throw new Error(`Role mapping missing for role id ${roleId}`)
+		}
+		return mapped
+	})
+}
+
+function toNonEmptyString(value) {
+	if (value === undefined || value === null) {
+		return ''
+	}
+	const normalized = String(value).trim()
+	return normalized
+}
+
+function isPlainObject(value) {
+	return value !== null && typeof value === 'object' && !Array.isArray(value)
+}
+
+function constructUrl(baseUrl, endPoint) {
+	if (!baseUrl || !endPoint) {
+		return baseUrl || endPoint || ''
+	}
+	const normalizedBase = String(baseUrl).replace(/\/+$/, '')
+	const normalizedPath = String(endPoint).replace(/^\/+/, '')
+	return `${normalizedBase}/${normalizedPath}`
+}
+
+function getExternalBaseUrl(service) {
+	if (!service) {
+		return ''
+	}
+	return (
+		process.env?.[`${service.toUpperCase()}_BASE_URL`] ||
+		process.env?.[`${service.replace(/-/g, '_').toUpperCase()}_BASE_URL`] ||
+		''
+	)
+}
+
+function chunkArray(input, size) {
+	const chunks = []
+	for (let i = 0; i < input.length; i += size) {
+		chunks.push(input.slice(i, i + size))
+	}
+	return chunks
+}
+
+function getMetaIds(value) {
+	if (value === undefined || value === null) {
+		return []
+	}
+	if (Array.isArray(value)) {
+		return value.flatMap((entry) => getMetaIds(entry))
+	}
+	if (isPlainObject(value)) {
+		if (value._id !== undefined && value._id !== null && toNonEmptyString(value._id) !== '') {
+			return [toNonEmptyString(value._id)]
+		}
+		if (value.id !== undefined && value.id !== null && toNonEmptyString(value.id) !== '') {
+			return [toNonEmptyString(value.id)]
+		}
+		if (value.value !== undefined && value.value !== null && toNonEmptyString(value.value) !== '') {
+			return [toNonEmptyString(value.value)]
+		}
+		return []
+	}
+	const normalized = toNonEmptyString(value)
+	return normalized ? [normalized] : []
+}
+
+function remapMetaValue(value, idMap) {
+	if (value === undefined || value === null) {
+		return value
+	}
+	if (Array.isArray(value)) {
+		return value.map((entry) => remapMetaValue(entry, idMap))
+	}
+	if (isPlainObject(value)) {
+		if (value._id !== undefined && value._id !== null) {
+			const key = toNonEmptyString(value._id)
+			return key && idMap.has(key) ? { ...value, _id: idMap.get(key) } : value
+		}
+		if (value.id !== undefined && value.id !== null) {
+			const key = toNonEmptyString(value.id)
+			return key && idMap.has(key) ? { ...value, id: idMap.get(key) } : value
+		}
+		if (value.value !== undefined && value.value !== null) {
+			const key = toNonEmptyString(value.value)
+			return key && idMap.has(key) ? { ...value, value: idMap.get(key) } : value
+		}
+		return value
+	}
+	const key = toNonEmptyString(value)
+	return key && idMap.has(key) ? idMap.get(key) : value
+}
+
+async function runInBatches(taskFactories, maxParallel) {
+	if (taskFactories.length === 0) {
+		return
+	}
+	let cursor = 0
+	const workers = Array.from({ length: Math.min(maxParallel, taskFactories.length) }, async () => {
+		while (cursor < taskFactories.length) {
+			const currentIndex = cursor
+			cursor += 1
+			await taskFactories[currentIndex]()
+		}
+	})
+	await Promise.all(workers)
+}
+
+async function lockByStrategy(sequelize, tx, options) {
+	if (options.lockStrategy === 'skip') {
+		return
+	}
+
+	const advisoryKey = `tenant-move:${options.currentTenantCode}:${options.currentOrgCode}:${options.newTenantCode}`
+	await querySelect(sequelize, 'SELECT pg_advisory_xact_lock(hashtext($advisoryKey));', { advisoryKey }, tx)
+
+	if (options.lockStrategy === 'advisory-table-lock') {
+		await queryRaw(
+			sequelize,
+			`LOCK TABLE users, user_organizations, user_organization_roles, user_sessions, user_roles, organization_role_requests, forms, entity_types, entities, notification_templates, organizations IN SHARE ROW EXCLUSIVE MODE;`,
+			{},
+			tx
+		)
+	}
+}
+
+function keyForRole(title, orgCode) {
+	return `${String(title || '')
+		.trim()
+		.toLowerCase()}|${String(orgCode || '')
+		.trim()
+		.toLowerCase()}`
+}
+
+async function loadExternalEntityTypeSpecs(sequelize, tx, context) {
+	const { sourceTenant, orgCode, sourceOrg, sourceDefaultOrg, defaultOrgCode } = context
+	const externalRows = await querySelect(
+		sequelize,
+		`SELECT id, value, data_type, meta, organization_code, organization_id
+		 FROM entity_types
+		 WHERE tenant_code = $sourceTenant
+		   AND external_entity_type = TRUE
+		   AND deleted_at IS NULL
+		   AND status = 'ACTIVE'
+		   AND organization_code = ANY($organizationCodes)
+		   AND organization_id = ANY($organizationIds)
+		 ORDER BY
+		   CASE
+		     WHEN organization_code = $orgCode THEN 0
+		     WHEN organization_code = $defaultOrgCode THEN 1
+		     ELSE 2
+		   END,
+		   id ASC;`,
+		{
+			sourceTenant,
+			orgCode,
+			defaultOrgCode,
+			organizationCodes: [orgCode, defaultOrgCode],
+			organizationIds: [Number(sourceOrg.id), Number(sourceDefaultOrg.id)],
+		},
+		tx
+	)
+
+	const sourceScopedCount = externalRows.filter((row) => row.organization_code === orgCode).length
+	const defaultScopedCount = externalRows.filter((row) => row.organization_code === defaultOrgCode).length
+
+	const externalEntityTypeSpecs = new Map()
+	for (const row of externalRows) {
+		const value = toNonEmptyString(row.value)
+		assertOrThrow(!!value, 'External entity_type has empty value', {
+			entityTypeId: row.id,
+			organizationCode: row.organization_code,
+		})
+
+		if (externalEntityTypeSpecs.has(value)) {
+			continue
+		}
+
+		const service = toNonEmptyString(row?.meta?.service)
+		const endPoint = toNonEmptyString(row?.meta?.endPoint)
+		assertOrThrow(!!service && !!endPoint, 'External entity_type meta.service/meta.endPoint missing', {
+			entityTypeId: row.id,
+			value,
+			organizationCode: row.organization_code,
+		})
+
+		externalEntityTypeSpecs.set(value, {
+			value,
+			dataType: row.data_type,
+			service,
+			endPoint,
+			sourceOrgScope: row.organization_code === orgCode ? 'source-org' : 'default-org',
+		})
+	}
+
+	return {
+		externalEntityTypeSpecs,
+		externalMetaKeys: Array.from(externalEntityTypeSpecs.keys()),
+		externalEntityTypeStats: {
+			sourceScopedCount,
+			defaultScopedCount,
+			dedupedCount: externalEntityTypeSpecs.size,
+		},
+	}
+}
+
+async function fetchExternalMappingsForGroup(group, targetTenant) {
+	const oldIds = Array.from(group.oldIds)
+	if (oldIds.length === 0) {
+		return new Map()
+	}
+
+	const externalBaseUrl = getExternalBaseUrl(group.service)
+	assertOrThrow(!!externalBaseUrl, 'External base URL missing for service', {
+		service: group.service,
+	})
+	const url = constructUrl(externalBaseUrl, group.endPoint)
+	assertOrThrow(!!url, 'External URL construction failed', {
+		service: group.service,
+		endPoint: group.endPoint,
+	})
+
+	const headers = {
+		'Content-Type': 'application/json',
+		'internal-access-token': process.env.INTERNAL_ACCESS_TOKEN,
+	}
+
+	const mappedIds = new Map()
+	const chunks = chunkArray(oldIds, EXTERNAL_FETCH_CHUNK_SIZE)
+	const taskFactories = chunks.map((chunk) => async () => {
+		const normalizedChunk = chunk.map((id) => toNonEmptyString(id)).filter(Boolean)
+		if (normalizedChunk.length === 0) {
+			return
+		}
+		const requestBody = {
+			query: {
+				'metaInformation.tenantMigrationReferenceId': {
+					$in: normalizedChunk,
+				},
+				tenantId: targetTenant,
+			},
+			projection: ['_id', 'metaInformation.tenantMigrationReferenceId'],
+		}
+
+		let page = 1
+		let fetchedCount = 0
+		let totalCount = null
+		let hasMore = true
+		while (hasMore) {
+			let response = null
+			try {
+				response = await axios.post(url, requestBody, {
+					headers,
+					params: {
+						page,
+						limit: EXTERNAL_FETCH_PAGE_LIMIT,
+					},
+				})
+			} catch (error) {
+				throw Object.assign(new Error('External mapping API call failed'), {
+					details: {
+						service: group.service,
+						endPoint: group.endPoint,
+						url,
+						page,
+						limit: EXTERNAL_FETCH_PAGE_LIMIT,
+						status: error?.response?.status || null,
+						data: error?.response?.data || null,
+						message: error.message,
+					},
+				})
+			}
+
+			const parsedCount = Number(response?.data?.count)
+			if (Number.isFinite(parsedCount) && parsedCount >= 0) {
+				totalCount = parsedCount
+			}
+
+			const result = Array.isArray(response?.data?.result) ? response.data.result : []
+			if (result.length === 0) {
+				hasMore = false
+				continue
+			}
+
+			for (const item of result) {
+				const oldId = toNonEmptyString(item?.metaInformation?.tenantMigrationReferenceId)
+				const newId = toNonEmptyString(item?._id)
+				if (!oldId || !newId) {
+					continue
+				}
+				if (!group.oldIds.has(oldId)) {
+					continue
+				}
+				if (mappedIds.has(oldId) && mappedIds.get(oldId) !== newId) {
+					throw Object.assign(new Error('Ambiguous external mapping found for old id'), {
+						details: {
+							oldId,
+							firstNewId: mappedIds.get(oldId),
+							secondNewId: newId,
+							service: group.service,
+							endPoint: group.endPoint,
+						},
+					})
+				}
+				mappedIds.set(oldId, newId)
+			}
+
+			fetchedCount += result.length
+			if (totalCount !== null && fetchedCount >= totalCount) {
+				hasMore = false
+				continue
+			}
+			if (result.length < EXTERNAL_FETCH_PAGE_LIMIT) {
+				hasMore = false
+				continue
+			}
+			page += 1
+		}
+	})
+
+	await runInBatches(taskFactories, EXTERNAL_FETCH_MAX_PARALLEL)
+
+	const missingIds = oldIds.filter((id) => !mappedIds.has(id))
+	assertOrThrow(missingIds.length === 0, 'External id mapping missing for user meta references', {
+		service: group.service,
+		endPoint: group.endPoint,
+		missingSample: missingIds.slice(0, 20),
+		missingCount: missingIds.length,
+	})
+
+	return mappedIds
+}
+
+async function remapExternalMetaForUsers(context) {
+	if (!context.externalEntityTypeSpecs || context.externalEntityTypeSpecs.size === 0) {
+		return {
+			usersForInsert: context.sourceUsers,
+			stats: {
+				affectedUsers: 0,
+				referencesDetected: 0,
+				referencesRemapped: 0,
+				groups: 0,
+			},
+		}
+	}
+
+	const groups = new Map()
+	const referencedIds = new Map()
+	let referencesDetected = 0
+	let affectedUsers = 0
+
+	for (const user of context.sourceUsers) {
+		if (!isPlainObject(user.meta)) {
+			continue
+		}
+
+		let userHasExternalMeta = false
+		for (const [metaKey, spec] of context.externalEntityTypeSpecs.entries()) {
+			if (!Object.prototype.hasOwnProperty.call(user.meta, metaKey)) {
+				continue
+			}
+			const ids = getMetaIds(user.meta[metaKey])
+			if (ids.length === 0) {
+				continue
+			}
+
+			userHasExternalMeta = true
+			referencesDetected += ids.length
+			referencedIds.set(metaKey, (referencedIds.get(metaKey) || 0) + ids.length)
+
+			const groupKey = `${spec.service}|${spec.endPoint}`
+			if (!groups.has(groupKey)) {
+				groups.set(groupKey, {
+					service: spec.service,
+					endPoint: spec.endPoint,
+					oldIds: new Set(),
+				})
+			}
+			const group = groups.get(groupKey)
+			ids.forEach((id) => group.oldIds.add(id))
+		}
+
+		if (userHasExternalMeta) {
+			affectedUsers += 1
+		}
+	}
+
+	if (groups.size === 0) {
+		return {
+			usersForInsert: context.sourceUsers,
+			stats: {
+				affectedUsers: 0,
+				referencesDetected: 0,
+				referencesRemapped: 0,
+				groups: 0,
+			},
+		}
+	}
+
+	const mappingByGroup = new Map()
+	for (const [groupKey, group] of groups.entries()) {
+		const groupMap = await fetchExternalMappingsForGroup(group, context.targetTenant)
+		mappingByGroup.set(groupKey, groupMap)
+	}
+
+	let referencesRemapped = 0
+	const usersForInsert = context.sourceUsers.map((user) => {
+		if (!isPlainObject(user.meta)) {
+			return user
+		}
+		const nextMeta = { ...user.meta }
+		let changed = false
+
+		for (const [metaKey, spec] of context.externalEntityTypeSpecs.entries()) {
+			if (!Object.prototype.hasOwnProperty.call(nextMeta, metaKey)) {
+				continue
+			}
+			const groupKey = `${spec.service}|${spec.endPoint}`
+			const groupMap = mappingByGroup.get(groupKey)
+			assertOrThrow(!!groupMap, 'Missing external mapping group for meta key', {
+				metaKey,
+				groupKey,
+			})
+
+			const existingIds = getMetaIds(nextMeta[metaKey])
+			existingIds.forEach((id) => {
+				assertOrThrow(groupMap.has(id), 'Missing external mapping for meta id during remap', {
+					metaKey,
+					id,
+					service: spec.service,
+					endPoint: spec.endPoint,
+				})
+			})
+			referencesRemapped += existingIds.length
+
+			const remappedValue = remapMetaValue(nextMeta[metaKey], groupMap)
+			nextMeta[metaKey] = remappedValue
+			changed = true
+		}
+
+		return changed ? { ...user, meta: nextMeta } : user
+	})
+
+	return {
+		usersForInsert,
+		stats: {
+			affectedUsers,
+			referencesDetected,
+			referencesRemapped,
+			groups: groups.size,
+			metaKeyReferenceCounts: Object.fromEntries(referencedIds),
+		},
+	}
+}
+
+async function buildContextAndPrecheck(sequelize, tx, options) {
+	const sourceTenant = options.currentTenantCode
+	const targetTenant = options.newTenantCode
+	const orgCode = options.currentOrgCode
+	const defaultOrgCode = toNonEmptyString(process.env.DEFAULT_ORGANISATION_CODE)
+	assertOrThrow(!!defaultOrgCode, 'DEFAULT_ORGANISATION_CODE is required for external entity-type fallback detection')
+
+	const sourceTenantRow = await querySelect(
+		sequelize,
+		'SELECT code FROM tenants WHERE code = $sourceTenant LIMIT 1;',
+		{ sourceTenant },
+		tx
+	)
+	const targetTenantRow = await querySelect(
+		sequelize,
+		'SELECT code FROM tenants WHERE code = $targetTenant LIMIT 1;',
+		{ targetTenant },
+		tx
+	)
+	assertOrThrow(sourceTenantRow.length === 1, 'Source tenant does not exist', { sourceTenant })
+	assertOrThrow(targetTenantRow.length === 1, 'Target tenant does not exist', { targetTenant })
+
+	const sourceOrgRows = await querySelect(
+		sequelize,
+		'SELECT id, code, tenant_code FROM organizations WHERE tenant_code = $sourceTenant AND code = $orgCode LIMIT 1;',
+		{ sourceTenant, orgCode },
+		tx
+	)
+	const targetOrgRows = await querySelect(
+		sequelize,
+		'SELECT id, code, tenant_code FROM organizations WHERE tenant_code = $targetTenant AND code = $orgCode LIMIT 1;',
+		{ targetTenant, orgCode },
+		tx
+	)
+	assertOrThrow(sourceOrgRows.length === 1, 'Source organization not found', { sourceTenant, orgCode })
+	assertOrThrow(targetOrgRows.length === 1, 'Target organization with same code not found', { targetTenant, orgCode })
+	const sourceDefaultOrgRows = await querySelect(
+		sequelize,
+		'SELECT id, code, tenant_code FROM organizations WHERE tenant_code = $sourceTenant AND code = $defaultOrgCode LIMIT 1;',
+		{ sourceTenant, defaultOrgCode },
+		tx
+	)
+	assertOrThrow(sourceDefaultOrgRows.length === 1, 'Default organization not found in source tenant', {
+		sourceTenant,
+		defaultOrgCode,
+	})
+
+	const sourceOrg = sourceOrgRows[0]
+	const targetOrg = targetOrgRows[0]
+	const sourceDefaultOrg = sourceDefaultOrgRows[0]
+
+	const scopedUsers = await querySelect(
+		sequelize,
+		`SELECT DISTINCT user_id
+		 FROM user_organizations
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND deleted_at IS NULL;`,
+		{ sourceTenant, orgCode },
+		tx
+	)
+
+	const userIds = scopedUsers.map((row) => Number(row.user_id))
+	assertOrThrow(userIds.length > 0, 'No users found in source tenant + organization', {
+		sourceTenant,
+		orgCode,
+	})
+
+	const crossOrgMembership = await querySelect(
+		sequelize,
+		`SELECT user_id, organization_code
+		 FROM user_organizations
+		 WHERE tenant_code = $sourceTenant
+		   AND user_id = ANY($userIds)
+		   AND organization_code <> $orgCode
+		 LIMIT 20;`,
+		{ sourceTenant, userIds, orgCode },
+		tx
+	)
+	assertOrThrow(
+		crossOrgMembership.length === 0,
+		'Users belong to multiple organizations in source tenant. Aborting.',
+		{
+			sample: crossOrgMembership,
+		}
+	)
+
+	const crossOrgRoles = await querySelect(
+		sequelize,
+		`SELECT user_id, organization_code, role_id
+		 FROM user_organization_roles
+		 WHERE tenant_code = $sourceTenant
+		   AND user_id = ANY($userIds)
+		   AND organization_code <> $orgCode
+		 LIMIT 20;`,
+		{ sourceTenant, userIds, orgCode },
+		tx
+	)
+	assertOrThrow(
+		crossOrgRoles.length === 0,
+		'User roles exist outside source organization for scoped users. Aborting.',
+		{
+			sample: crossOrgRoles,
+		}
+	)
+
+	const roleRequestRefs = await querySelect(
+		sequelize,
+		`SELECT id, requester_id, handled_by
+		 FROM organization_role_requests
+		 WHERE tenant_code = $sourceTenant
+		   AND (requester_id = ANY($userIds) OR handled_by = ANY($userIds))
+		 LIMIT 20;`,
+		{ sourceTenant, userIds },
+		tx
+	)
+	assertOrThrow(
+		roleRequestRefs.length === 0,
+		'Scoped users are referenced in organization_role_requests. Aborting.',
+		{
+			sample: roleRequestRefs,
+		}
+	)
+
+	const targetUserIdCollision = await querySelect(
+		sequelize,
+		`SELECT id
+		 FROM users
+		 WHERE tenant_code = $targetTenant
+		   AND id = ANY($userIds)
+		 LIMIT 20;`,
+		{ targetTenant, userIds },
+		tx
+	)
+	assertOrThrow(targetUserIdCollision.length === 0, '(id, newTenantCode) already exists in target users', {
+		sample: targetUserIdCollision,
+	})
+
+	const usernameCollision = await querySelect(
+		sequelize,
+		`SELECT su.id, su.username
+		 FROM users su
+		 JOIN users tu
+		   ON tu.tenant_code = $targetTenant
+		  AND tu.username = su.username
+		 WHERE su.tenant_code = $sourceTenant
+		   AND su.id = ANY($userIds)
+		   AND su.username IS NOT NULL
+		 LIMIT 20;`,
+		{ sourceTenant, targetTenant, userIds },
+		tx
+	)
+	assertOrThrow(usernameCollision.length === 0, 'Username conflict in target tenant', { sample: usernameCollision })
+
+	const phoneCollision = await querySelect(
+		sequelize,
+		`SELECT su.id, su.phone
+		 FROM users su
+		 JOIN users tu
+		   ON tu.tenant_code = $targetTenant
+		  AND tu.phone = su.phone
+		 WHERE su.tenant_code = $sourceTenant
+		   AND su.id = ANY($userIds)
+		   AND su.phone IS NOT NULL
+		 LIMIT 20;`,
+		{ sourceTenant, targetTenant, userIds },
+		tx
+	)
+	assertOrThrow(phoneCollision.length === 0, 'Phone conflict in target tenant', { sample: phoneCollision })
+
+	const sourceForms = await querySelect(
+		sequelize,
+		`SELECT id, type, sub_type
+		 FROM forms
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_id = $sourceOrgId;`,
+		{ sourceTenant, sourceOrgId: sourceOrg.id },
+		tx
+	)
+
+	const sourceEntityTypesMismatch = await querySelect(
+		sequelize,
+		`SELECT id
+		 FROM entity_types
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND organization_id <> $sourceOrgId
+		 LIMIT 1;`,
+		{ sourceTenant, orgCode, sourceOrgId: sourceOrg.id },
+		tx
+	)
+	assertOrThrow(sourceEntityTypesMismatch.length === 0, 'entity_types org code/id mismatch in source scope', {})
+
+	const defaultEntityTypesMismatch = await querySelect(
+		sequelize,
+		`SELECT id
+		 FROM entity_types
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $defaultOrgCode
+		   AND organization_id <> $sourceDefaultOrgId
+		 LIMIT 1;`,
+		{
+			sourceTenant,
+			defaultOrgCode,
+			sourceDefaultOrgId: sourceDefaultOrg.id,
+		},
+		tx
+	)
+	assertOrThrow(
+		defaultEntityTypesMismatch.length === 0,
+		'entity_types org code/id mismatch in default-org fallback scope',
+		{}
+	)
+
+	const sourceEntityTypes = await querySelect(
+		sequelize,
+		`SELECT id, parent_id, value, deleted_at
+		 FROM entity_types
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND organization_id = $sourceOrgId;`,
+		{ sourceTenant, orgCode, sourceOrgId: sourceOrg.id },
+		tx
+	)
+
+	const sourceEntityTypeIds = new Set(sourceEntityTypes.map((row) => Number(row.id)))
+	const brokenParent = sourceEntityTypes.find(
+		(row) => row.parent_id !== null && !sourceEntityTypeIds.has(Number(row.parent_id))
+	)
+	assertOrThrow(!brokenParent, 'entity_types parent_id points outside scoped source entity_types', {
+		entityTypeId: brokenParent?.id,
+		parentId: brokenParent?.parent_id,
+	})
+
+	const sourceEntities = await querySelect(
+		sequelize,
+		`SELECT id, entity_type_id, value, deleted_at
+		 FROM entities
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode;`,
+		{ sourceTenant, orgCode },
+		tx
+	)
+
+	const entitiesOutsideEntityTypeScope = sourceEntities.find(
+		(row) => !sourceEntityTypeIds.has(Number(row.entity_type_id))
+	)
+	assertOrThrow(
+		!entitiesOutsideEntityTypeScope,
+		'Entity references entity_type not present in scoped source entity_types',
+		{
+			entityId: entitiesOutsideEntityTypeScope?.id,
+			entityTypeId: entitiesOutsideEntityTypeScope?.entity_type_id,
+		}
+	)
+
+	const sourceNotificationTemplates = await querySelect(
+		sequelize,
+		`SELECT id, type, code
+		 FROM notification_templates
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode;`,
+		{ sourceTenant, orgCode },
+		tx
+	)
+
+	const sourceFormIds = sourceForms.map((row) => Number(row.id))
+	const sourceEntityTypeIdArray = sourceEntityTypes.map((row) => Number(row.id))
+	const sourceEntityIds = sourceEntities.map((row) => Number(row.id))
+	const sourceTemplateIds = sourceNotificationTemplates.map((row) => Number(row.id))
+
+	if (sourceFormIds.length > 0) {
+		const formPkCollision = await querySelect(
+			sequelize,
+			`SELECT id
+			 FROM forms
+			 WHERE tenant_code = $targetTenant
+			   AND id = ANY($sourceFormIds)
+			 LIMIT 20;`,
+			{ targetTenant, sourceFormIds },
+			tx
+		)
+		assertOrThrow(formPkCollision.length === 0, 'forms id collision in target tenant', { sample: formPkCollision })
+
+		const formUniqueCollision = await querySelect(
+			sequelize,
+			`SELECT f.id, f.type, f.sub_type
+			 FROM forms f
+			 JOIN forms s
+			   ON s.tenant_code = $sourceTenant
+			  AND s.organization_id = $sourceOrgId
+			  AND f.type = s.type
+			  AND f.sub_type = s.sub_type
+			 WHERE f.tenant_code = $targetTenant
+			   AND f.organization_id = $targetOrgId
+			 LIMIT 20;`,
+			{ sourceTenant, targetTenant, sourceOrgId: sourceOrg.id, targetOrgId: targetOrg.id },
+			tx
+		)
+		assertOrThrow(formUniqueCollision.length === 0, 'forms unique key collision in target tenant/org', {
+			sample: formUniqueCollision,
+		})
+	}
+
+	if (sourceEntityTypeIdArray.length > 0) {
+		const entityTypePkCollision = await querySelect(
+			sequelize,
+			`SELECT id
+			 FROM entity_types
+			 WHERE tenant_code = $targetTenant
+			   AND id = ANY($sourceEntityTypeIds)
+			 LIMIT 20;`,
+			{ targetTenant, sourceEntityTypeIds: sourceEntityTypeIdArray },
+			tx
+		)
+		assertOrThrow(entityTypePkCollision.length === 0, 'entity_types id collision in target tenant', {
+			sample: entityTypePkCollision,
+		})
+
+		const entityTypeValueCollision = await querySelect(
+			sequelize,
+			`SELECT t.id, t.value
+			 FROM entity_types t
+			 JOIN entity_types s
+			   ON s.tenant_code = $sourceTenant
+			  AND s.organization_code = $orgCode
+			  AND s.organization_id = $sourceOrgId
+			  AND s.deleted_at IS NULL
+			  AND t.value = s.value
+			 WHERE t.tenant_code = $targetTenant
+			   AND t.organization_code = $orgCode
+			   AND t.organization_id = $targetOrgId
+			   AND t.deleted_at IS NULL
+			 LIMIT 20;`,
+			{
+				sourceTenant,
+				targetTenant,
+				orgCode,
+				sourceOrgId: sourceOrg.id,
+				targetOrgId: targetOrg.id,
+			},
+			tx
+		)
+		assertOrThrow(
+			entityTypeValueCollision.length === 0,
+			'entity_types active value collision in target tenant/org',
+			{
+				sample: entityTypeValueCollision,
+			}
+		)
+	}
+
+	if (sourceEntityIds.length > 0) {
+		const entityPkCollision = await querySelect(
+			sequelize,
+			`SELECT id
+			 FROM entities
+			 WHERE tenant_code = $targetTenant
+			   AND id = ANY($sourceEntityIds)
+			 LIMIT 20;`,
+			{ targetTenant, sourceEntityIds },
+			tx
+		)
+		assertOrThrow(entityPkCollision.length === 0, 'entities id collision in target tenant', {
+			sample: entityPkCollision,
+		})
+
+		const entityUniqueCollision = await querySelect(
+			sequelize,
+			`SELECT t.id, t.value, t.entity_type_id
+			 FROM entities t
+			 JOIN entities s
+			   ON s.tenant_code = $sourceTenant
+			  AND s.organization_code = $orgCode
+			  AND s.deleted_at IS NULL
+			  AND t.value = s.value
+			  AND t.entity_type_id = s.entity_type_id
+			 WHERE t.tenant_code = $targetTenant
+			   AND t.organization_code = $orgCode
+			   AND t.deleted_at IS NULL
+			 LIMIT 20;`,
+			{ sourceTenant, targetTenant, orgCode },
+			tx
+		)
+		assertOrThrow(entityUniqueCollision.length === 0, 'entities active unique-key collision in target tenant/org', {
+			sample: entityUniqueCollision,
+		})
+	}
+
+	if (sourceTemplateIds.length > 0) {
+		const notificationPkCollision = await querySelect(
+			sequelize,
+			`SELECT id
+			 FROM notification_templates
+			 WHERE tenant_code = $targetTenant
+			   AND id = ANY($sourceTemplateIds)
+			 LIMIT 20;`,
+			{ targetTenant, sourceTemplateIds },
+			tx
+		)
+		assertOrThrow(notificationPkCollision.length === 0, 'notification_templates id collision in target tenant', {
+			sample: notificationPkCollision,
+		})
+
+		const notificationUniqueCollision = await querySelect(
+			sequelize,
+			`SELECT t.id, t.type, t.code
+			 FROM notification_templates t
+			 JOIN notification_templates s
+			   ON s.tenant_code = $sourceTenant
+			  AND s.organization_code = $orgCode
+			  AND t.type = s.type
+			  AND t.code = s.code
+			 WHERE t.tenant_code = $targetTenant
+			   AND t.organization_code = $orgCode
+			 LIMIT 20;`,
+			{ sourceTenant, targetTenant, orgCode },
+			tx
+		)
+		assertOrThrow(
+			notificationUniqueCollision.length === 0,
+			'notification_templates unique key collision in target tenant/org',
+			{ sample: notificationUniqueCollision }
+		)
+	}
+
+	const sourceRolesFromUor = await querySelect(
+		sequelize,
+		`SELECT DISTINCT role_id
+		 FROM user_organization_roles
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND user_id = ANY($userIds);`,
+		{ sourceTenant, orgCode, userIds },
+		tx
+	)
+
+	const sourceRolesFromUsers = await querySelect(
+		sequelize,
+		`SELECT DISTINCT unnest(roles) AS role_id
+		 FROM users
+		 WHERE tenant_code = $sourceTenant
+		   AND id = ANY($userIds);`,
+		{ sourceTenant, userIds },
+		tx
+	)
+
+	const requiredRoleIds = new Set()
+	sourceRolesFromUor.forEach((row) => row.role_id !== null && requiredRoleIds.add(Number(row.role_id)))
+	sourceRolesFromUsers.forEach((row) => row.role_id !== null && requiredRoleIds.add(Number(row.role_id)))
+	const requiredRoleIdArray = Array.from(requiredRoleIds)
+
+	const sourceRoleRows =
+		requiredRoleIdArray.length > 0
+			? await querySelect(
+					sequelize,
+					`SELECT ur.id, ur.title, ur.deleted_at, ur.organization_id, o.code AS organization_code
+					 FROM user_roles ur
+					 JOIN organizations o
+					   ON o.id = ur.organization_id
+					  AND o.tenant_code = ur.tenant_code
+					 WHERE ur.tenant_code = $sourceTenant
+					   AND ur.id = ANY($requiredRoleIdArray);`,
+					{ sourceTenant, requiredRoleIdArray },
+					tx
+			  )
+			: []
+
+	assertOrThrow(sourceRoleRows.length === requiredRoleIdArray.length, 'Referenced source role not found', {
+		requiredRoleCount: requiredRoleIdArray.length,
+		foundRoleCount: sourceRoleRows.length,
+	})
+
+	const sourceRoleById = new Map(sourceRoleRows.map((row) => [Number(row.id), row]))
+
+	const sourceUsers = await querySelect(
+		sequelize,
+		`SELECT id, name, email, email_verified, roles, status, password, has_accepted_terms_and_conditions, about, location, languages, preferred_language, share_link, image, custom_entity_text, meta, created_at, updated_at, deleted_at, phone, phone_code, username, configs
+		 FROM users
+		 WHERE tenant_code = $sourceTenant
+		   AND id = ANY($userIds);`,
+		{ sourceTenant, userIds },
+		tx
+	)
+	assertOrThrow(sourceUsers.length === userIds.length, 'Some scoped users are missing in users table', {
+		scopedUserCount: userIds.length,
+		foundUserCount: sourceUsers.length,
+	})
+
+	const externalSpecData = await loadExternalEntityTypeSpecs(sequelize, tx, {
+		sourceTenant,
+		orgCode,
+		sourceOrg,
+		sourceDefaultOrg,
+		defaultOrgCode,
+	})
+
+	const sourceUserOrganizations = await querySelect(
+		sequelize,
+		`SELECT user_id, organization_code, created_at, updated_at, deleted_at
+		 FROM user_organizations
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND user_id = ANY($userIds);`,
+		{ sourceTenant, orgCode, userIds },
+		tx
+	)
+
+	const sourceUserOrganizationRoles = await querySelect(
+		sequelize,
+		`SELECT user_id, organization_code, role_id, created_at, updated_at, deleted_at
+		 FROM user_organization_roles
+		 WHERE tenant_code = $sourceTenant
+		   AND organization_code = $orgCode
+		   AND user_id = ANY($userIds);`,
+		{ sourceTenant, orgCode, userIds },
+		tx
+	)
+
+	const sourceUserSessions = await querySelect(
+		sequelize,
+		`SELECT id, user_id, started_at, ended_at, token, device_info, refresh_token, created_at, updated_at, deleted_at
+		 FROM user_sessions
+		 WHERE tenant_code = $sourceTenant
+		   AND user_id = ANY($userIds);`,
+		{ sourceTenant, userIds },
+		tx
+	)
+
+	const sourceCounts = {
+		users: sourceUsers.length,
+		user_organizations: sourceUserOrganizations.length,
+		user_organization_roles: sourceUserOrganizationRoles.length,
+		user_sessions: sourceUserSessions.length,
+		forms: sourceForms.length,
+		entity_types: sourceEntityTypes.length,
+		entities: sourceEntities.length,
+		notification_templates: sourceNotificationTemplates.length,
+	}
+
+	return {
+		sourceTenant,
+		targetTenant,
+		orgCode,
+		defaultOrgCode,
+		sourceOrg,
+		sourceDefaultOrg,
+		targetOrg,
+		userIds,
+		sourceUsers,
+		sourceUserOrganizations,
+		sourceUserOrganizationRoles,
+		sourceUserSessions,
+		sourceForms,
+		sourceEntityTypes,
+		sourceEntities,
+		sourceNotificationTemplates,
+		sourceCounts,
+		requiredRoleIdArray,
+		sourceRoleById,
+		externalEntityTypeSpecs: externalSpecData.externalEntityTypeSpecs,
+		externalMetaKeys: externalSpecData.externalMetaKeys,
+		externalEntityTypeStats: externalSpecData.externalEntityTypeStats,
+	}
+}
+
+async function ensureStrictIdCompatibility(sequelize, tx, context, options) {
+	const { targetTenant, sourceRoleById, requiredRoleIdArray } = context
+
+	const compatibilityResult = {
+		performedRebase: false,
+		roleMap: new Map(requiredRoleIdArray.map((roleId) => [roleId, roleId])),
+	}
+
+	if (requiredRoleIdArray.length === 0) {
+		return compatibilityResult
+	}
+
+	const targetRolesByIdRows = await querySelect(
+		sequelize,
+		`SELECT ur.id, ur.title, ur.deleted_at, o.code AS organization_code
+		 FROM user_roles ur
+		 JOIN organizations o
+		   ON o.id = ur.organization_id
+		  AND o.tenant_code = ur.tenant_code
+		 WHERE ur.tenant_code = $targetTenant
+		   AND ur.deleted_at IS NULL
+		   AND ur.id = ANY($requiredRoleIdArray);`,
+		{ targetTenant, requiredRoleIdArray },
+		tx
+	)
+
+	const targetRoleById = new Map(targetRolesByIdRows.map((row) => [Number(row.id), row]))
+
+	let compatible = true
+	for (const roleId of requiredRoleIdArray) {
+		const sourceRole = sourceRoleById.get(roleId)
+		const targetRole = targetRoleById.get(roleId)
+		if (!targetRole) {
+			compatible = false
+			break
+		}
+		if (
+			String(sourceRole.title).toLowerCase() !== String(targetRole.title).toLowerCase() ||
+			String(sourceRole.organization_code).toLowerCase() !== String(targetRole.organization_code).toLowerCase()
+		) {
+			compatible = false
+			break
+		}
+	}
+
+	if (compatible) {
+		return compatibilityResult
+	}
+
+	assertOrThrow(
+		options.strictIdRebase === 'if-target-tenant-empty',
+		'Strict-id is incompatible and strict-id-rebase is set to never'
+	)
+
+	const targetCounts = await querySelect(
+		sequelize,
+		`SELECT
+			(SELECT COUNT(*)::int FROM users WHERE tenant_code = $targetTenant) AS users_count,
+			(SELECT COUNT(*)::int FROM user_organizations WHERE tenant_code = $targetTenant) AS user_org_count,
+			(SELECT COUNT(*)::int FROM user_organization_roles WHERE tenant_code = $targetTenant) AS user_org_role_count,
+			(SELECT COUNT(*)::int FROM organization_role_requests WHERE tenant_code = $targetTenant) AS org_role_req_count;`,
+		{ targetTenant },
+		tx
+	)
+
+	const countRow = targetCounts[0]
+	assertOrThrow(
+		Number(countRow.users_count) === 0 &&
+			Number(countRow.user_org_count) === 0 &&
+			Number(countRow.user_org_role_count) === 0 &&
+			Number(countRow.org_role_req_count) === 0,
+		'Strict-id rebase allowed only when target tenant has no users/relations',
+		{ targetCounts: countRow }
+	)
+
+	const targetActiveRoles = await querySelect(
+		sequelize,
+		`SELECT ur.id, ur.title, o.code AS organization_code
+		 FROM user_roles ur
+		 JOIN organizations o
+		   ON o.id = ur.organization_id
+		  AND o.tenant_code = ur.tenant_code
+		 WHERE ur.tenant_code = $targetTenant
+		   AND ur.deleted_at IS NULL;`,
+		{ targetTenant },
+		tx
+	)
+
+	const targetRoleKeyMap = new Map()
+	for (const targetRole of targetActiveRoles) {
+		const key = keyForRole(targetRole.title, targetRole.organization_code)
+		if (!targetRoleKeyMap.has(key)) {
+			targetRoleKeyMap.set(key, [])
+		}
+		targetRoleKeyMap.get(key).push(targetRole)
+	}
+
+	const rolesNeedingRebase = requiredRoleIdArray.filter((roleId) => {
+		const sourceRole = sourceRoleById.get(roleId)
+		const targetRole = targetRoleById.get(roleId)
+		return !(
+			targetRole &&
+			String(sourceRole.title).toLowerCase() === String(targetRole.title).toLowerCase() &&
+			String(sourceRole.organization_code).toLowerCase() === String(targetRole.organization_code).toLowerCase()
+		)
+	})
+
+	const targetToSourceIdMap = []
+	for (const sourceRoleId of rolesNeedingRebase) {
+		const sourceRole = sourceRoleById.get(sourceRoleId)
+		const key = keyForRole(sourceRole.title, sourceRole.organization_code)
+		const matchedTargetRoles = targetRoleKeyMap.get(key) || []
+		assertOrThrow(
+			matchedTargetRoles.length === 1,
+			'Unable to uniquely map required source role to target role for strict-id rebase',
+			{
+				sourceRoleId,
+				title: sourceRole.title,
+				organizationCode: sourceRole.organization_code,
+				matchCount: matchedTargetRoles.length,
+			}
+		)
+		targetToSourceIdMap.push({
+			oldId: Number(matchedTargetRoles[0].id),
+			newId: Number(sourceRole.id),
+			title: sourceRole.title,
+			organizationCode: sourceRole.organization_code,
+		})
+	}
+
+	const dedupByNewId = new Set(targetToSourceIdMap.map((row) => row.newId))
+	assertOrThrow(
+		dedupByNewId.size === targetToSourceIdMap.length,
+		'Strict-id rebase generated duplicate desired role ids for required roles'
+	)
+
+	const desiredIds = targetToSourceIdMap.filter((row) => row.oldId !== row.newId).map((row) => row.newId)
+	if (desiredIds.length > 0) {
+		const collisionWithDeleted = await querySelect(
+			sequelize,
+			`SELECT id, title, deleted_at
+			 FROM user_roles
+			 WHERE tenant_code = $targetTenant
+			   AND deleted_at IS NOT NULL
+			   AND id = ANY($desiredIds)
+			 LIMIT 20;`,
+			{ targetTenant, desiredIds },
+			tx
+		)
+		assertOrThrow(
+			collisionWithDeleted.length === 0,
+			'Strict-id rebase would collide with existing deleted roles in target tenant',
+			{ sample: collisionWithDeleted }
+		)
+	}
+
+	const mappedOldIds = new Set(targetToSourceIdMap.filter((row) => row.oldId !== row.newId).map((row) => row.oldId))
+	const blockingRows = desiredIds.length
+		? await querySelect(
+				sequelize,
+				`SELECT id
+				 FROM user_roles
+				 WHERE tenant_code = $targetTenant
+				   AND deleted_at IS NULL
+				   AND id = ANY($desiredIds)
+				   AND NOT (id = ANY($mappedOldIds));`,
+				{
+					targetTenant,
+					desiredIds,
+					mappedOldIds: Array.from(mappedOldIds),
+				},
+				tx
+		  )
+		: []
+
+	let nextFreeRoleId = 0
+	if (blockingRows.length > 0) {
+		// NOTE: Assigning IDs above MAX(id) here. If this path is exercised,
+		// the backing sequence for user_roles.id must be reset after the rebase
+		// (setval to COALESCE(MAX(id),1)) to prevent future insert collisions.
+		const maxIdRows = await querySelect(
+			sequelize,
+			`SELECT COALESCE(MAX(id), 0) AS max_id
+			 FROM user_roles
+			 WHERE tenant_code = $targetTenant;`,
+			{ targetTenant },
+			tx
+		)
+		nextFreeRoleId = Number(maxIdRows[0].max_id) + 1
+	}
+
+	for (const blocker of blockingRows) {
+		await queryRaw(
+			sequelize,
+			`UPDATE user_roles
+			 SET id = $newId
+			 WHERE tenant_code = $targetTenant
+			   AND deleted_at IS NULL
+			   AND id = $oldId;`,
+			{
+				targetTenant,
+				oldId: Number(blocker.id),
+				newId: nextFreeRoleId,
+			},
+			tx
+		)
+		nextFreeRoleId += 1
+	}
+
+	let tempSeed = -1000000000
+	for (const mapping of targetToSourceIdMap) {
+		if (mapping.oldId === mapping.newId) {
+			continue
+		}
+		await queryRaw(
+			sequelize,
+			`UPDATE user_roles
+			 SET id = $tmpId
+			 WHERE tenant_code = $targetTenant
+			   AND deleted_at IS NULL
+			   AND id = $oldId;`,
+			{ tmpId: tempSeed, targetTenant, oldId: mapping.oldId },
+			tx
+		)
+		mapping.tmpId = tempSeed
+		tempSeed -= 1
+	}
+
+	for (const mapping of targetToSourceIdMap) {
+		if (mapping.oldId === mapping.newId) {
+			continue
+		}
+		await queryRaw(
+			sequelize,
+			`UPDATE user_roles
+			 SET id = $newId
+			 WHERE tenant_code = $targetTenant
+			   AND deleted_at IS NULL
+			   AND id = $tmpId;`,
+			{ newId: mapping.newId, targetTenant, tmpId: mapping.tmpId },
+			tx
+		)
+	}
+
+	const verifyRows = await querySelect(
+		sequelize,
+		`SELECT ur.id, ur.title, o.code AS organization_code
+		 FROM user_roles ur
+		 JOIN organizations o
+		   ON o.id = ur.organization_id
+		  AND o.tenant_code = ur.tenant_code
+		 WHERE ur.tenant_code = $targetTenant
+		   AND ur.deleted_at IS NULL
+		   AND ur.id = ANY($requiredRoleIdArray);`,
+		{ targetTenant, requiredRoleIdArray },
+		tx
+	)
+
+	const verifyMap = new Map(verifyRows.map((row) => [Number(row.id), row]))
+	for (const roleId of requiredRoleIdArray) {
+		const sourceRole = sourceRoleById.get(roleId)
+		const targetRole = verifyMap.get(roleId)
+		assertOrThrow(!!targetRole, 'Strict-id rebase did not materialize required role id in target tenant', {
+			roleId,
+		})
+		assertOrThrow(
+			String(sourceRole.title).toLowerCase() === String(targetRole.title).toLowerCase() &&
+				String(sourceRole.organization_code).toLowerCase() ===
+					String(targetRole.organization_code).toLowerCase(),
+			'Strict-id rebase semantic mismatch for role id',
+			{ roleId, sourceRole, targetRole }
+		)
+	}
+
+	compatibilityResult.roleMap = new Map(requiredRoleIdArray.map((roleId) => [Number(roleId), Number(roleId)]))
+	compatibilityResult.performedRebase = true
+	return compatibilityResult
+}
+
+async function buildMapByTitleRoleMap(sequelize, tx, context) {
+	const { targetTenant, requiredRoleIdArray, sourceRoleById } = context
+	const roleMap = new Map()
+
+	for (const sourceRoleId of requiredRoleIdArray) {
+		const sourceRole = sourceRoleById.get(sourceRoleId)
+		const matched = await querySelect(
+			sequelize,
+			`SELECT ur.id
+			 FROM user_roles ur
+			 JOIN organizations o
+			   ON o.id = ur.organization_id
+			  AND o.tenant_code = ur.tenant_code
+			 WHERE ur.tenant_code = $targetTenant
+			   AND o.code = $organizationCode
+			   AND LOWER(ur.title) = LOWER($title)
+			   AND ur.deleted_at IS NULL;`,
+			{
+				targetTenant,
+				organizationCode: sourceRole.organization_code,
+				title: sourceRole.title,
+			},
+			tx
+		)
+
+		assertOrThrow(matched.length === 1, 'Role map-by-title is missing or ambiguous', {
+			sourceRoleId,
+			title: sourceRole.title,
+			organizationCode: sourceRole.organization_code,
+			matchCount: matched.length,
+		})
+
+		roleMap.set(sourceRoleId, Number(matched[0].id))
+	}
+
+	return roleMap
+}
+
+async function copyOrgScopedTables(sequelize, tx, context) {
+	const { sourceTenant, targetTenant, orgCode, sourceOrg, targetOrg } = context
+	const results = {
+		formsCopied: 0,
+		entityTypesCopied: 0,
+		entityTypesParentPatched: 0,
+		entitiesCopied: 0,
+		notificationTemplatesCopied: 0,
+	}
+
+	const insertedForms = await querySelect(
+		sequelize,
+		`INSERT INTO forms (
+			id, type, sub_type, data, version, created_at, updated_at, deleted_at, organization_id, tenant_code
+		)
+		SELECT
+			id, type, sub_type, data, version, created_at, updated_at, deleted_at, $targetOrgId, $targetTenant
+		FROM forms
+		WHERE tenant_code = $sourceTenant
+		  AND organization_id = $sourceOrgId
+		RETURNING id;`,
+		{
+			sourceTenant,
+			targetTenant,
+			sourceOrgId: sourceOrg.id,
+			targetOrgId: targetOrg.id,
+		},
+		tx
+	)
+	results.formsCopied = insertedForms.length
+
+	const insertedEntityTypes = await querySelect(
+		sequelize,
+		`INSERT INTO entity_types (
+			id, value, label, status, created_by, updated_by, allow_filtering, data_type, organization_id, parent_id,
+			has_entities, allow_custom_entities, model_names, created_at, updated_at, deleted_at, meta,
+			external_entity_type, required, regex, tenant_code, organization_code
+		)
+		SELECT
+			id, value, label, status, created_by, updated_by, allow_filtering, data_type, $targetOrgId, NULL,
+			has_entities, allow_custom_entities, model_names, created_at, updated_at, deleted_at, meta,
+			external_entity_type, required, regex, $targetTenant, organization_code
+		FROM entity_types
+		WHERE tenant_code = $sourceTenant
+		  AND organization_code = $orgCode
+		  AND organization_id = $sourceOrgId
+		RETURNING id;`,
+		{
+			sourceTenant,
+			targetTenant,
+			orgCode,
+			sourceOrgId: sourceOrg.id,
+			targetOrgId: targetOrg.id,
+		},
+		tx
+	)
+	results.entityTypesCopied = insertedEntityTypes.length
+
+	const patchedEntityTypeParents = await querySelect(
+		sequelize,
+		`UPDATE entity_types tgt
+		SET parent_id = src.parent_id
+		FROM entity_types src
+		WHERE tgt.tenant_code = $targetTenant
+		  AND tgt.organization_code = $orgCode
+		  AND tgt.organization_id = $targetOrgId
+		  AND src.tenant_code = $sourceTenant
+		  AND src.organization_code = $orgCode
+		  AND src.organization_id = $sourceOrgId
+		  AND tgt.id = src.id
+		  AND src.parent_id IS NOT NULL
+		RETURNING tgt.id;`,
+		{
+			sourceTenant,
+			targetTenant,
+			orgCode,
+			sourceOrgId: sourceOrg.id,
+			targetOrgId: targetOrg.id,
+		},
+		tx
+	)
+	results.entityTypesParentPatched = patchedEntityTypeParents.length
+
+	const insertedEntities = await querySelect(
+		sequelize,
+		`INSERT INTO entities (
+			id, entity_type_id, value, label, status, type, created_by, updated_by, created_at, updated_at, deleted_at, tenant_code, organization_code
+		)
+		SELECT
+			id, entity_type_id, value, label, status, type, created_by, updated_by, created_at, updated_at, deleted_at, $targetTenant, organization_code
+		FROM entities
+		WHERE tenant_code = $sourceTenant
+		  AND organization_code = $orgCode
+		RETURNING id;`,
+		{ sourceTenant, targetTenant, orgCode },
+		tx
+	)
+	results.entitiesCopied = insertedEntities.length
+
+	const insertedNotificationTemplates = await querySelect(
+		sequelize,
+		`INSERT INTO notification_templates (
+			id, type, code, subject, body, status, email_header, email_footer, created_by, updated_by, created_at, updated_at, deleted_at, tenant_code, organization_code
+		)
+		SELECT
+			id, type, code, subject, body, status, email_header, email_footer, created_by, updated_by, created_at, updated_at, deleted_at, $targetTenant, organization_code
+		FROM notification_templates
+		WHERE tenant_code = $sourceTenant
+		  AND organization_code = $orgCode
+		RETURNING id;`,
+		{ sourceTenant, targetTenant, orgCode },
+		tx
+	)
+	results.notificationTemplatesCopied = insertedNotificationTemplates.length
+
+	return results
+}
+
+async function moveUserScopedTables(sequelize, tx, context, roleMap, options) {
+	const {
+		sourceTenant,
+		targetTenant,
+		sourceUsers,
+		usersForInsert,
+		sourceUserOrganizations,
+		sourceUserOrganizationRoles,
+	} = context
+	const results = {
+		usersInserted: 0,
+		userOrganizationsInserted: 0,
+		userOrganizationRolesInserted: 0,
+		userSessionsInserted: 0,
+		sourceSessionsDeleted: 0,
+	}
+
+	const userRowsToInsert = Array.isArray(usersForInsert) ? usersForInsert : sourceUsers
+	for (const user of userRowsToInsert) {
+		const mappedRoles = mapRoleArray(user.roles, roleMap)
+		const insertUserRows = await querySelect(
+			sequelize,
+			`INSERT INTO users (
+				id, name, email, email_verified, roles, status, password, has_accepted_terms_and_conditions, about, location, languages, preferred_language, share_link, image, custom_entity_text, meta, created_at, updated_at, deleted_at, tenant_code, phone, phone_code, username, configs
+			) VALUES (
+				$id, $name, $email, $emailVerified, $roles, $status, $password, $hasAcceptedTnC, $about, $location, $languages, $preferredLanguage, $shareLink, $image, $customEntityText, $meta, $createdAt, $updatedAt, $deletedAt, $targetTenant, $phone, $phoneCode, $username, $configs
+			) RETURNING id;`,
+			{
+				id: user.id,
+				name: user.name,
+				email: user.email,
+				emailVerified: user.email_verified,
+				roles: mappedRoles,
+				status: user.status,
+				password: user.password,
+				hasAcceptedTnC: user.has_accepted_terms_and_conditions,
+				about: user.about,
+				location: user.location,
+				languages: user.languages,
+				preferredLanguage: user.preferred_language,
+				shareLink: user.share_link,
+				image: user.image,
+				customEntityText: user.custom_entity_text,
+				meta: user.meta,
+				createdAt: user.created_at,
+				updatedAt: user.updated_at,
+				deletedAt: user.deleted_at,
+				targetTenant,
+				phone: user.phone,
+				phoneCode: user.phone_code,
+				username: user.username,
+				configs: user.configs,
+			},
+			tx
+		)
+		results.usersInserted += insertUserRows.length
+	}
+
+	for (const row of sourceUserOrganizations) {
+		const inserted = await querySelect(
+			sequelize,
+			`INSERT INTO user_organizations (
+				user_id, organization_code, tenant_code, created_at, updated_at, deleted_at
+			) VALUES (
+				$userId, $orgCode, $targetTenant, $createdAt, $updatedAt, $deletedAt
+			) RETURNING user_id;`,
+			{
+				userId: row.user_id,
+				orgCode: row.organization_code,
+				targetTenant,
+				createdAt: row.created_at,
+				updatedAt: row.updated_at,
+				deletedAt: row.deleted_at,
+			},
+			tx
+		)
+		results.userOrganizationsInserted += inserted.length
+	}
+
+	for (const row of sourceUserOrganizationRoles) {
+		const mappedRoleId = roleMap.get(Number(row.role_id))
+		assertOrThrow(!!mappedRoleId, 'Role mapping missing for user_organization_roles row', {
+			roleId: row.role_id,
+		})
+
+		const inserted = await querySelect(
+			sequelize,
+			`INSERT INTO user_organization_roles (
+				tenant_code, user_id, organization_code, role_id, created_at, updated_at, deleted_at
+			) VALUES (
+				$targetTenant, $userId, $orgCode, $roleId, $createdAt, $updatedAt, $deletedAt
+			) RETURNING user_id;`,
+			{
+				targetTenant,
+				userId: row.user_id,
+				orgCode: row.organization_code,
+				roleId: mappedRoleId,
+				createdAt: row.created_at,
+				updatedAt: row.updated_at,
+				deletedAt: row.deleted_at,
+			},
+			tx
+		)
+		results.userOrganizationRolesInserted += inserted.length
+	}
+
+	if (options.sessionMode === 'migrate') {
+		const insertedSessions = await querySelect(
+			sequelize,
+			`INSERT INTO user_sessions (
+				id, user_id, started_at, ended_at, token, device_info, refresh_token, created_at, updated_at, deleted_at, tenant_code
+			)
+			SELECT
+				id, user_id, started_at, ended_at, token, device_info, refresh_token, created_at, updated_at, deleted_at, $targetTenant
+			FROM user_sessions
+			WHERE tenant_code = $sourceTenant
+			  AND user_id = ANY($userIds)
+			RETURNING id;`,
+			{
+				sourceTenant,
+				targetTenant,
+				userIds: context.userIds,
+			},
+			tx
+		)
+		results.userSessionsInserted = insertedSessions.length
+	}
+
+	const deletedSessions = await querySelect(
+		sequelize,
+		`DELETE FROM user_sessions
+		 WHERE tenant_code = $sourceTenant
+		   AND user_id = ANY($userIds)
+		 RETURNING id;`,
+		{
+			sourceTenant,
+			userIds: context.userIds,
+		},
+		tx
+	)
+	results.sourceSessionsDeleted = deletedSessions.length
+
+	return results
+}
+
+async function cleanupSourceRows(sequelize, tx, context, options) {
+	const { sourceTenant, orgCode, sourceOrg, userIds } = context
+	const now = new Date()
+	const results = {
+		userRowsAffected: 0,
+		orgRowsAffected: 0,
+	}
+
+	if (options.deleteMode === 'none') {
+		return results
+	}
+
+	if (options.deleteMode === 'soft') {
+		const softUor = await querySelect(
+			sequelize,
+			`UPDATE user_organization_roles
+			 SET deleted_at = COALESCE(deleted_at, $now)
+			 WHERE tenant_code = $sourceTenant
+			   AND organization_code = $orgCode
+			   AND user_id = ANY($userIds)
+			 RETURNING user_id;`,
+			{ now, sourceTenant, orgCode, userIds },
+			tx
+		)
+		results.userRowsAffected += softUor.length
+
+		const softUo = await querySelect(
+			sequelize,
+			`UPDATE user_organizations
+			 SET deleted_at = COALESCE(deleted_at, $now)
+			 WHERE tenant_code = $sourceTenant
+			   AND organization_code = $orgCode
+			   AND user_id = ANY($userIds)
+			 RETURNING user_id;`,
+			{ now, sourceTenant, orgCode, userIds },
+			tx
+		)
+		results.userRowsAffected += softUo.length
+
+		const softUsers = await querySelect(
+			sequelize,
+			`UPDATE users
+			 SET deleted_at = COALESCE(deleted_at, $now)
+			 WHERE tenant_code = $sourceTenant
+			   AND id = ANY($userIds)
+			 RETURNING id;`,
+			{ now, sourceTenant, userIds },
+			tx
+		)
+		results.userRowsAffected += softUsers.length
+
+		if (options.deleteScope === 'all-copied') {
+			const softForms = await querySelect(
+				sequelize,
+				`UPDATE forms
+				 SET deleted_at = COALESCE(deleted_at, $now)
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_id = $sourceOrgId
+				 RETURNING id;`,
+				{ now, sourceTenant, sourceOrgId: sourceOrg.id },
+				tx
+			)
+			results.orgRowsAffected += softForms.length
+
+			const softEntityTypes = await querySelect(
+				sequelize,
+				`UPDATE entity_types
+				 SET deleted_at = COALESCE(deleted_at, $now)
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				   AND organization_id = $sourceOrgId
+				 RETURNING id;`,
+				{ now, sourceTenant, orgCode, sourceOrgId: sourceOrg.id },
+				tx
+			)
+			results.orgRowsAffected += softEntityTypes.length
+
+			const softEntities = await querySelect(
+				sequelize,
+				`UPDATE entities
+				 SET deleted_at = COALESCE(deleted_at, $now)
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				 RETURNING id;`,
+				{ now, sourceTenant, orgCode },
+				tx
+			)
+			results.orgRowsAffected += softEntities.length
+
+			const softTemplates = await querySelect(
+				sequelize,
+				`UPDATE notification_templates
+				 SET deleted_at = COALESCE(deleted_at, $now)
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				 RETURNING id;`,
+				{ now, sourceTenant, orgCode },
+				tx
+			)
+			results.orgRowsAffected += softTemplates.length
+		}
+	}
+
+	if (options.deleteMode === 'hard') {
+		const delUor = await querySelect(
+			sequelize,
+			`DELETE FROM user_organization_roles
+			 WHERE tenant_code = $sourceTenant
+			   AND organization_code = $orgCode
+			   AND user_id = ANY($userIds)
+			 RETURNING user_id;`,
+			{ sourceTenant, orgCode, userIds },
+			tx
+		)
+		results.userRowsAffected += delUor.length
+
+		const delUo = await querySelect(
+			sequelize,
+			`DELETE FROM user_organizations
+			 WHERE tenant_code = $sourceTenant
+			   AND organization_code = $orgCode
+			   AND user_id = ANY($userIds)
+			 RETURNING user_id;`,
+			{ sourceTenant, orgCode, userIds },
+			tx
+		)
+		results.userRowsAffected += delUo.length
+
+		const delUsers = await querySelect(
+			sequelize,
+			`DELETE FROM users
+			 WHERE tenant_code = $sourceTenant
+			   AND id = ANY($userIds)
+			 RETURNING id;`,
+			{ sourceTenant, userIds },
+			tx
+		)
+		results.userRowsAffected += delUsers.length
+
+		if (options.deleteScope === 'all-copied') {
+			const delEntities = await querySelect(
+				sequelize,
+				`DELETE FROM entities
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				 RETURNING id;`,
+				{ sourceTenant, orgCode },
+				tx
+			)
+			results.orgRowsAffected += delEntities.length
+
+			const delEntityTypes = await querySelect(
+				sequelize,
+				`DELETE FROM entity_types
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				   AND organization_id = $sourceOrgId
+				 RETURNING id;`,
+				{ sourceTenant, orgCode, sourceOrgId: sourceOrg.id },
+				tx
+			)
+			results.orgRowsAffected += delEntityTypes.length
+
+			const delForms = await querySelect(
+				sequelize,
+				`DELETE FROM forms
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_id = $sourceOrgId
+				 RETURNING id;`,
+				{ sourceTenant, sourceOrgId: sourceOrg.id },
+				tx
+			)
+			results.orgRowsAffected += delForms.length
+
+			const delTemplates = await querySelect(
+				sequelize,
+				`DELETE FROM notification_templates
+				 WHERE tenant_code = $sourceTenant
+				   AND organization_code = $orgCode
+				 RETURNING id;`,
+				{ sourceTenant, orgCode },
+				tx
+			)
+			results.orgRowsAffected += delTemplates.length
+		}
+	}
+
+	return results
+}
+
+async function postValidate(sequelize, tx, context, operationCounts, options, roleMap) {
+	const { targetTenant, sourceTenant, orgCode, targetOrg, userIds, sourceCounts, sourceUsers } = context
+
+	const targetUserCount = await querySelect(
+		sequelize,
+		`SELECT COUNT(*)::int AS count
+		 FROM users
+		 WHERE tenant_code = $targetTenant
+		   AND id = ANY($userIds);`,
+		{ targetTenant, userIds },
+		tx
+	)
+	assertOrThrow(
+		Number(targetUserCount[0].count) === sourceCounts.users,
+		'Post-validation failed for users count in target',
+		{
+			expected: sourceCounts.users,
+			actual: Number(targetUserCount[0].count),
+		}
+	)
+
+	const targetUoCount = await querySelect(
+		sequelize,
+		`SELECT COUNT(*)::int AS count
+		 FROM user_organizations
+		 WHERE tenant_code = $targetTenant
+		   AND organization_code = $orgCode
+		   AND user_id = ANY($userIds);`,
+		{ targetTenant, orgCode, userIds },
+		tx
+	)
+	assertOrThrow(
+		Number(targetUoCount[0].count) === sourceCounts.user_organizations,
+		'Post-validation failed for user_organizations count in target',
+		{
+			expected: sourceCounts.user_organizations,
+			actual: Number(targetUoCount[0].count),
+		}
+	)
+
+	const targetUorCount = await querySelect(
+		sequelize,
+		`SELECT COUNT(*)::int AS count
+		 FROM user_organization_roles
+		 WHERE tenant_code = $targetTenant
+		   AND organization_code = $orgCode
+		   AND user_id = ANY($userIds);`,
+		{ targetTenant, orgCode, userIds },
+		tx
+	)
+	assertOrThrow(
+		Number(targetUorCount[0].count) === sourceCounts.user_organization_roles,
+		'Post-validation failed for user_organization_roles count in target',
+		{
+			expected: sourceCounts.user_organization_roles,
+			actual: Number(targetUorCount[0].count),
+		}
+	)
+
+	if (options.sessionMode === 'migrate') {
+		const targetSessionCount = await querySelect(
+			sequelize,
+			`SELECT COUNT(*)::int AS count
+			 FROM user_sessions
+			 WHERE tenant_code = $targetTenant
+			   AND user_id = ANY($userIds);`,
+			{ targetTenant, userIds },
+			tx
+		)
+		assertOrThrow(
+			Number(targetSessionCount[0].count) === sourceCounts.user_sessions,
+			'Post-validation failed for user_sessions count in target',
+			{
+				expected: sourceCounts.user_sessions,
+				actual: Number(targetSessionCount[0].count),
+			}
+		)
+	}
+
+	const sourceSessionCount = await querySelect(
+		sequelize,
+		`SELECT COUNT(*)::int AS count
+		 FROM user_sessions
+		 WHERE tenant_code = $sourceTenant
+		   AND user_id = ANY($userIds);`,
+		{ sourceTenant, userIds },
+		tx
+	)
+	assertOrThrow(Number(sourceSessionCount[0].count) === 0, 'Source sessions still exist after session handling', {
+		count: Number(sourceSessionCount[0].count),
+	})
+
+	const sourceFormIds = context.sourceForms.map((row) => Number(row.id))
+	const targetFormsCount =
+		sourceFormIds.length > 0
+			? await querySelect(
+					sequelize,
+					`SELECT COUNT(*)::int AS count
+					 FROM forms
+					 WHERE tenant_code = $targetTenant
+					   AND organization_id = $targetOrgId
+					   AND id = ANY($sourceFormIds);`,
+					{
+						targetTenant,
+						targetOrgId: targetOrg.id,
+						sourceFormIds,
+					},
+					tx
+			  )
+			: [{ count: 0 }]
+	assertOrThrow(
+		Number(targetFormsCount[0].count) === sourceCounts.forms,
+		'Post-validation failed for forms count in target',
+		{
+			expected: sourceCounts.forms,
+			actual: Number(targetFormsCount[0].count),
+		}
+	)
+
+	const sourceEntityTypeIds = context.sourceEntityTypes.map((row) => Number(row.id))
+	const targetEntityTypesCount =
+		sourceEntityTypeIds.length > 0
+			? await querySelect(
+					sequelize,
+					`SELECT COUNT(*)::int AS count
+					 FROM entity_types
+					 WHERE tenant_code = $targetTenant
+					   AND organization_code = $orgCode
+					   AND organization_id = $targetOrgId
+					   AND id = ANY($sourceEntityTypeIds);`,
+					{
+						targetTenant,
+						targetOrgId: targetOrg.id,
+						orgCode,
+						sourceEntityTypeIds,
+					},
+					tx
+			  )
+			: [{ count: 0 }]
+	assertOrThrow(
+		Number(targetEntityTypesCount[0].count) === sourceCounts.entity_types,
+		'Post-validation failed for entity_types count in target',
+		{
+			expected: sourceCounts.entity_types,
+			actual: Number(targetEntityTypesCount[0].count),
+		}
+	)
+
+	const sourceEntityIds = context.sourceEntities.map((row) => Number(row.id))
+	const targetEntitiesCount =
+		sourceEntityIds.length > 0
+			? await querySelect(
+					sequelize,
+					`SELECT COUNT(*)::int AS count
+					 FROM entities
+					 WHERE tenant_code = $targetTenant
+					   AND organization_code = $orgCode
+					   AND id = ANY($sourceEntityIds);`,
+					{
+						targetTenant,
+						orgCode,
+						sourceEntityIds,
+					},
+					tx
+			  )
+			: [{ count: 0 }]
+	assertOrThrow(
+		Number(targetEntitiesCount[0].count) === sourceCounts.entities,
+		'Post-validation failed for entities count in target',
+		{
+			expected: sourceCounts.entities,
+			actual: Number(targetEntitiesCount[0].count),
+		}
+	)
+
+	const sourceTemplateIds = context.sourceNotificationTemplates.map((row) => Number(row.id))
+	const targetTemplatesCount =
+		sourceTemplateIds.length > 0
+			? await querySelect(
+					sequelize,
+					`SELECT COUNT(*)::int AS count
+					 FROM notification_templates
+					 WHERE tenant_code = $targetTenant
+					   AND organization_code = $orgCode
+					   AND id = ANY($sourceTemplateIds);`,
+					{
+						targetTenant,
+						orgCode,
+						sourceTemplateIds,
+					},
+					tx
+			  )
+			: [{ count: 0 }]
+	assertOrThrow(
+		Number(targetTemplatesCount[0].count) === sourceCounts.notification_templates,
+		'Post-validation failed for notification_templates count in target',
+		{
+			expected: sourceCounts.notification_templates,
+			actual: Number(targetTemplatesCount[0].count),
+		}
+	)
+
+	const targetUsers = await querySelect(
+		sequelize,
+		`SELECT id, roles
+		 FROM users
+		 WHERE tenant_code = $targetTenant
+		   AND id = ANY($userIds);`,
+		{ targetTenant, userIds },
+		tx
+	)
+	const sourceUsersById = new Map(sourceUsers.map((row) => [Number(row.id), row]))
+	for (const targetUser of targetUsers) {
+		const sourceUser = sourceUsersById.get(Number(targetUser.id))
+		const expected = mapRoleArray(sourceUser.roles, roleMap)
+		const actual = (targetUser.roles || []).map((roleId) => Number(roleId))
+		const matches =
+			expected.length === actual.length &&
+			expected.every((roleId, index) => Number(roleId) === Number(actual[index]))
+		assertOrThrow(matches, 'users.roles mismatch after migration', {
+			userId: targetUser.id,
+			expected,
+			actual,
+		})
+	}
+
+	if (options.deleteMode === 'hard') {
+		const sourceUsersRemaining = await querySelect(
+			sequelize,
+			`SELECT COUNT(*)::int AS count
+			 FROM users
+			 WHERE tenant_code = $sourceTenant
+			   AND id = ANY($userIds);`,
+			{ sourceTenant, userIds },
+			tx
+		)
+		assertOrThrow(
+			Number(sourceUsersRemaining[0].count) === 0,
+			'Hard delete selected but source users still exist',
+			{}
+		)
+	}
+
+	if (options.deleteMode === 'soft') {
+		const sourceUsersSoft = await querySelect(
+			sequelize,
+			`SELECT COUNT(*)::int AS count
+			 FROM users
+			 WHERE tenant_code = $sourceTenant
+			   AND id = ANY($userIds)
+			   AND deleted_at IS NOT NULL;`,
+			{ sourceTenant, userIds },
+			tx
+		)
+		assertOrThrow(
+			Number(sourceUsersSoft[0].count) === sourceCounts.users,
+			'Soft delete selected but source users are not soft-deleted as expected',
+			{}
+		)
+	}
+
+	log('info', 'post_validation_success', {
+		operationCounts,
+	})
+}
+
+async function run() {
+	const args = minimist(process.argv.slice(2), {
+		string: [
+			'current-tenant-code',
+			'current-org-code',
+			'new-tenant-code',
+			'role-resolution',
+			'strict-id-rebase',
+			'delete-mode',
+			'delete-scope',
+			'session-mode',
+			'lock-strategy',
+			'dry-run',
+		],
+		boolean: ['help'],
+	})
+
+	if (args.help) {
+		printUsage()
+		return
+	}
+
+	const environmentData = require('../envVariables')()
+	if (!environmentData.success) {
+		console.error('Server could not start. Not all environment variables are provided.')
+		process.exit(1)
+	}
+
+	const rawOptions = {
+		currentTenantCode: getOption(args['current-tenant-code'], process.env.MIGRATION_CURRENT_TENANT_CODE, ''),
+		currentOrgCode: getOption(args['current-org-code'], process.env.MIGRATION_CURRENT_ORG_CODE, ''),
+		newTenantCode: getOption(args['new-tenant-code'], process.env.MIGRATION_NEW_TENANT_CODE, ''),
+		roleResolution: getOption(
+			args['role-resolution'],
+			process.env.MIGRATION_ROLE_RESOLUTION,
+			DEFAULTS.roleResolution
+		),
+		strictIdRebase: getOption(
+			args['strict-id-rebase'],
+			process.env.MIGRATION_STRICT_ID_REBASE,
+			DEFAULTS.strictIdRebase
+		),
+		deleteMode: getOption(args['delete-mode'], process.env.MIGRATION_DELETE_MODE, DEFAULTS.deleteMode),
+		deleteScope: getOption(args['delete-scope'], process.env.MIGRATION_DELETE_SCOPE, DEFAULTS.deleteScope),
+		sessionMode: getOption(args['session-mode'], process.env.MIGRATION_SESSION_MODE, DEFAULTS.sessionMode),
+		lockStrategy: getOption(args['lock-strategy'], process.env.MIGRATION_LOCK_STRATEGY, DEFAULTS.lockStrategy),
+		dryRun: getOption(args['dry-run'], process.env.MIGRATION_DRY_RUN, DEFAULTS.dryRun),
+	}
+
+	const options = normalizeAndValidateOptions(rawOptions)
+
+	const nodeEnv = getNodeEnv()
+	const databaseUrl = getDatabaseUrl(nodeEnv)
+	assertOrThrow(!!databaseUrl, 'Database URL not found. Set DEV/TEST/PROD_DATABASE_URL or DATABASE_URL.', { nodeEnv })
+
+	log('info', 'migration_start', {
+		nodeEnv,
+		sourceTenant: options.currentTenantCode,
+		sourceOrgCode: options.currentOrgCode,
+		targetTenant: options.newTenantCode,
+		roleResolution: options.roleResolution,
+		strictIdRebase: options.strictIdRebase,
+		deleteMode: options.deleteMode,
+		deleteScope: options.deleteScope,
+		sessionMode: options.sessionMode,
+		lockStrategy: options.lockStrategy,
+		dryRun: options.dryRun,
+	})
+
+	const sequelize = new Sequelize(databaseUrl, {
+		dialect: 'postgres',
+		logging: false,
+	})
+
+	let tx = null
+	try {
+		await sequelize.authenticate()
+		log('info', 'db_connection_success')
+
+		tx = await sequelize.transaction({
+			isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
+		})
+		log('info', 'transaction_started', { isolationLevel: 'SERIALIZABLE' })
+
+		await lockByStrategy(sequelize, tx, options)
+		log('info', 'lock_strategy_applied', { lockStrategy: options.lockStrategy })
+
+		const context = await buildContextAndPrecheck(sequelize, tx, options)
+		log('info', 'precheck_success', {
+			sourceOrgId: context.sourceOrg.id,
+			targetOrgId: context.targetOrg.id,
+			userCount: context.sourceCounts.users,
+			sourceCounts: context.sourceCounts,
+		})
+		log('info', 'default_org_resolved', {
+			sourceTenant: context.sourceTenant,
+			defaultOrgCode: context.defaultOrgCode,
+			defaultOrgId: context.sourceDefaultOrg.id,
+		})
+		log('info', 'external_entity_type_detection_summary', {
+			sourceTenant: context.sourceTenant,
+			sourceOrgCode: context.orgCode,
+			defaultOrgCode: context.defaultOrgCode,
+			sourceScopedCount: context.externalEntityTypeStats.sourceScopedCount,
+			defaultScopedCount: context.externalEntityTypeStats.defaultScopedCount,
+			dedupedFinalKeyCount: context.externalEntityTypeStats.dedupedCount,
+			externalMetaKeys: context.externalMetaKeys,
+		})
+
+		let roleMap = new Map(context.requiredRoleIdArray.map((roleId) => [roleId, roleId]))
+		let performedStrictIdRebase = false
+
+		if (options.roleResolution === 'strict-id') {
+			const strictResult = await ensureStrictIdCompatibility(sequelize, tx, context, options)
+			roleMap = strictResult.roleMap
+			performedStrictIdRebase = strictResult.performedRebase
+		} else {
+			roleMap = await buildMapByTitleRoleMap(sequelize, tx, context)
+		}
+
+		log('info', 'role_resolution_ready', {
+			roleResolution: options.roleResolution,
+			requiredRoleCount: context.requiredRoleIdArray.length,
+			mapSize: roleMap.size,
+			performedStrictIdRebase,
+		})
+
+		const externalRemapResult = await remapExternalMetaForUsers(context)
+		context.usersForInsert = externalRemapResult.usersForInsert
+		log('info', 'external_meta_remap_ready', externalRemapResult.stats)
+
+		if (options.dryRun) {
+			await tx.rollback()
+			tx = null
+			log('info', 'dry_run_complete', {
+				sourceCounts: context.sourceCounts,
+				roleMapSize: roleMap.size,
+				externalMetaRemap: externalRemapResult.stats,
+				userTables: USER_TABLES,
+				orgTables: ORG_TABLES,
+			})
+			return
+		}
+
+		const orgCopyCounts = await copyOrgScopedTables(sequelize, tx, context)
+		log('info', 'org_tables_copied', orgCopyCounts)
+
+		const userMoveCounts = await moveUserScopedTables(sequelize, tx, context, roleMap, options)
+		log('info', 'user_tables_moved', userMoveCounts)
+
+		const cleanupCounts = await cleanupSourceRows(sequelize, tx, context, options)
+		log('info', 'source_cleanup_complete', cleanupCounts)
+
+		await postValidate(
+			sequelize,
+			tx,
+			context,
+			{ ...orgCopyCounts, ...userMoveCounts, ...cleanupCounts },
+			options,
+			roleMap
+		)
+
+		await tx.commit()
+		tx = null
+		log('info', 'migration_commit_success')
+	} catch (error) {
+		if (tx) {
+			try {
+				await tx.rollback()
+			} catch (rollbackError) {
+				log('error', 'rollback_failed', {
+					message: rollbackError.message,
+					stack: rollbackError.stack,
+				})
+			}
+		}
+		log('error', 'migration_failed', {
+			message: error.message,
+			sqlState: error?.original?.code || null,
+			details: error?.details || null,
+			stack: error.stack,
+		})
+		process.exitCode = 1
+	} finally {
+		try {
+			await sequelize.close()
+		} catch (closeError) {
+			log('error', 'db_close_failed', {
+				message: closeError.message,
+			})
+		}
+	}
+}
+
+run()
diff --git a/src/scripts/readme.md b/src/scripts/readme.md
index 060c6fc3f..fb4bac988 100644
--- a/src/scripts/readme.md
+++ b/src/scripts/readme.md
@@ -28,3 +28,23 @@
     ```
     node ./encryptDecryptEmails.js decrypt
     ```
+
+## Tenant Org Data Migration Script
+
+-   Run from `src/`:
+
+    ```bash
+    node scripts/migrateTenantOrgData.js \
+      --current-tenant-code=<sourceTenant> \
+      --current-org-code=<orgCode> \
+      --new-tenant-code=<targetTenant>
+    ```
+
+-   Optional flags:
+    -   `--role-resolution=strict-id|map-by-title`
+    -   `--strict-id-rebase=if-target-tenant-empty|never`
+    -   `--delete-mode=soft|hard|none`
+    -   `--delete-scope=users-only|all-copied`
+    -   `--session-mode=invalidate|migrate`
+    -   `--lock-strategy=skip|advisory-only|advisory-table-lock`
+    -   `--dry-run=true|false`
diff --git a/src/services/account.js b/src/services/account.js
index 323c9d1c8..4640c2f5e 100644
--- a/src/services/account.js
+++ b/src/services/account.js
@@ -40,6 +40,39 @@ const notificationUtils = require('@utils/notification')
 const userHelper = require('@helpers/userHelper')
 const { broadcastEvent } = require('@helpers/eventBroadcasterMain')
 
+const AUTH_MODES = common.AUTH_MODES
+const OTP_PURPOSES = common.OTP_PURPOSES
+
+function otpRedisKey(purpose, encryptedIdentifier) {
+	return `${purpose}:${encryptedIdentifier}`
+}
+
+function resolveOtpPurpose({ username, registrationCode, userExists }) {
+	if (username) return OTP_PURPOSES.LOGIN
+	if (registrationCode) return OTP_PURPOSES.SIGNUP
+	if (userExists) return OTP_PURPOSES.LOGIN
+	return OTP_PURPOSES.SIGNUP
+}
+
+async function getExistingOtpForPurpose(purpose, identifiers) {
+	for (const identifier of identifiers) {
+		const data = await utilsHelper.redisGet(otpRedisKey(purpose, identifier))
+		if (data && data.action === purpose) {
+			return data
+		}
+	}
+	return null
+}
+
+async function setOtpForPurpose(purpose, identifiers, redisData) {
+	const redisSetResults = []
+	for (const identifier of identifiers) {
+		const result = await utilsHelper.redisSet(otpRedisKey(purpose, identifier), redisData, common.otpExpirationTime)
+		redisSetResults.push(result)
+	}
+	return redisSetResults
+}
+
 module.exports = class AccountHelper {
 	/**
 	 * create account
@@ -81,6 +114,29 @@ module.exports = class AccountHelper {
 				return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
 			}
 
+			const config = tenantDetail.configuration || {}
+			const allowedAuthMode = Array.isArray(config.allowed_auth_mode) ? config.allowed_auth_mode : []
+			const otpPurpose = bodyData._otpPurpose || OTP_PURPOSES.SIGNUP
+			delete bodyData._otpPurpose
+			const passwordAuthAllowed = allowedAuthMode.includes(AUTH_MODES.PASSWORD)
+
+			// #TODO: Below checks has to be moved to validator file
+			if (bodyData.password && !passwordAuthAllowed) {
+				return responses.failureResponse({
+					message: 'AUTH_MODE_NOT_ALLOWED',
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
+			}
+
+			if (passwordAuthAllowed && !bodyData.password) {
+				return responses.failureResponse({
+					message: 'PASSWORD_REQUIRED',
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
+			}
+
 			if (!bodyData.email && !bodyData.phone) {
 				return responses.failureResponse({
 					message: 'EMAIL_OR_PHONE_REQUIRED',
@@ -146,7 +202,6 @@ module.exports = class AccountHelper {
 			let user = await userQueries.findOne(
 				{
 					[Op.or]: criteria,
-					password: { [Op.ne]: null },
 					tenant_code: tenantDetail.code,
 				},
 				{
@@ -167,9 +222,17 @@ module.exports = class AccountHelper {
 				let isOtpValid = false
 				const providedOtp = bodyData.otp
 
+				if (!providedOtp) {
+					return responses.failureResponse({
+						message: 'OTP_REQUIRED',
+						statusCode: httpStatusCode.bad_request,
+						responseCode: 'CLIENT_ERROR',
+					})
+				}
+
 				// Check email OTP if email is provided
 				if (encryptedEmailId) {
-					const emailRedisData = await utilsHelper.redisGet(encryptedEmailId)
+					const emailRedisData = await utilsHelper.redisGet(otpRedisKey(otpPurpose, encryptedEmailId))
 					if (emailRedisData && emailRedisData.otp === providedOtp) {
 						isOtpValid = true
 					}
@@ -177,7 +240,9 @@ module.exports = class AccountHelper {
 
 				// Check phone OTP if phone is provided
 				if (encryptedPhoneNumber) {
-					const phoneRedisData = await utilsHelper.redisGet(bodyData.phone_code + encryptedPhoneNumber)
+					const phoneRedisData = await utilsHelper.redisGet(
+						otpRedisKey(otpPurpose, bodyData.phone_code + encryptedPhoneNumber)
+					)
 					if (phoneRedisData && phoneRedisData.otp === providedOtp) {
 						isOtpValid = true
 					}
@@ -192,7 +257,7 @@ module.exports = class AccountHelper {
 				}
 			}
 
-			bodyData.password = utilsHelper.hashPassword(bodyData.password)
+			bodyData.password = bodyData.password ? utilsHelper.hashPassword(bodyData.password) : null
 			if (!bodyData.username) {
 				bodyData.username = await generateUniqueUsername(bodyData.name)
 			}
@@ -552,8 +617,9 @@ module.exports = class AccountHelper {
 			)
 
 			// Delete Redis OTP entries
-			if (encryptedEmailId) await utilsHelper.redisDel(encryptedEmailId)
-			if (encryptedPhoneNumber) await utilsHelper.redisDel(bodyData.phone_code + encryptedPhoneNumber)
+			if (encryptedEmailId) await utilsHelper.redisDel(otpRedisKey(otpPurpose, encryptedEmailId))
+			if (encryptedPhoneNumber)
+				await utilsHelper.redisDel(otpRedisKey(otpPurpose, bodyData.phone_code + encryptedPhoneNumber))
 
 			if (isOrgAdmin) {
 				let organization = await organizationQueries.findByPk(user.organization_id)
@@ -576,7 +642,7 @@ module.exports = class AccountHelper {
 					emailId: plaintextEmailId,
 					templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE,
 					variables: {
-						name: bodyData.name,
+						name: bodyData.name || common.USER,
 						appName: tenantDetail.name,
 						roles: roleToString || '',
 						portalURL: tenantDomain.domain,
@@ -592,7 +658,7 @@ module.exports = class AccountHelper {
 					phoneNumber: plaintextPhoneNumber,
 					templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE,
 					variables: {
-						name: bodyData.name,
+						name: bodyData.name || common.USER,
 						appName: tenantDetail.name,
 						roles: roleToString || '',
 						portalURL: tenantDomain.domain,
@@ -664,6 +730,13 @@ module.exports = class AccountHelper {
 					responseCode: 'CLIENT_ERROR',
 				})
 
+			const badRequestResponse = (message) =>
+				responses.failureResponse({
+					message,
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
+
 			// Validate tenant domain
 			const tenantDomain = await tenantDomainQueries.findOne({ domain })
 			if (!tenantDomain) {
@@ -678,67 +751,114 @@ module.exports = class AccountHelper {
 				return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN')
 			}
 
-			// Helper functions to detect identifier type
-			const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
-			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed
-			const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str)
+			const config = tenantDetail.configuration || {}
+			const allowedAuthMode = Array.isArray(config.allowed_auth_mode) ? config.allowed_auth_mode : []
+			const hasOtp = Boolean(bodyData.otp)
+			const hasPassword = Boolean(bodyData.password)
+			const otpAllowed = allowedAuthMode.includes(AUTH_MODES.OTP)
+			const passwordAllowed = allowedAuthMode.includes(AUTH_MODES.PASSWORD)
+
+			if (!hasOtp && !hasPassword) {
+				return badRequestResponse('AUTH_CREDENTIAL_REQUIRED')
+			}
+
+			if ((hasOtp && !otpAllowed) || (hasPassword && !passwordAllowed)) {
+				return badRequestResponse('AUTH_MODE_NOT_ALLOWED')
+			}
 
 			const identifier = bodyData.identifier?.toLowerCase()
 			if (!identifier) {
-				return responses.failureResponse({
-					message: 'IDENTIFIER_REQUIRED',
-					statusCode: httpStatusCode.bad_request,
-					responseCode: 'CLIENT_ERROR',
-				})
+				return badRequestResponse('IDENTIFIER_REQUIRED')
+			}
+
+			const type = utils.checkIdentifierType(identifier)
+			if (!type) {
+				return badRequestResponse('INVALID_IDENTIFIER_FORMAT')
 			}
 
 			// Prepare query based on identifier type
 			const query = {
 				[Op.or]: [],
-				password: { [Op.ne]: null },
 				status: common.ACTIVE_STATUS,
 				tenant_code: tenantDetail.code,
 			}
 
-			if (isEmail(identifier)) {
-				query[Op.or].push({ email: emailEncryption.encrypt(identifier) })
-			} else if (isPhone(identifier)) {
-				query[Op.or].push({ phone: emailEncryption.encrypt(identifier), phone_code: bodyData.phone_code }) // Adjust if phone encryption differs
+			let encryptedIdentifier = null
+			if (type === common.EMAIL) {
+				encryptedIdentifier = emailEncryption.encrypt(identifier)
+				query[Op.or].push({ email: encryptedIdentifier })
+			} else if (type === common.PHONE) {
+				encryptedIdentifier = emailEncryption.encrypt(identifier)
+				query[Op.or].push({ phone: encryptedIdentifier, phone_code: bodyData.phone_code })
 			} else {
+				encryptedIdentifier = identifier
 				query[Op.or].push({ username: identifier })
 			}
+
+			const redisIdentifier =
+				type === common.PHONE ? `${bodyData.phone_code}${encryptedIdentifier}` : encryptedIdentifier
+			let isOtpValid = false
+
 			// Find user
 			const userInstance = await userQueries.findUserWithOrganization(query, {}, true)
 			let user = userInstance ? userInstance.toJSON() : null
+			const purpose = user ? OTP_PURPOSES.LOGIN : OTP_PURPOSES.SIGNUP
+
+			// #TODO: Handling OTP check for users created by admin
+			if (hasOtp) {
+				const redisData = await utilsHelper.redisGet(otpRedisKey(purpose, redisIdentifier))
+				isOtpValid = Boolean(redisData && redisData.otp === bodyData.otp)
+				if (!isOtpValid) {
+					return badRequestResponse('OTP_INVALID')
+				}
+			}
 
 			if (!user) {
-				return responses.failureResponse({
-					message: 'IDENTIFIER_OR_PASSWORD_INVALID',
-					statusCode: httpStatusCode.bad_request,
-					responseCode: 'CLIENT_ERROR',
-				})
+				if (config.auto_register && hasOtp && isOtpValid && allowedAuthMode.includes(AUTH_MODES.OTP)) {
+					if (allowedAuthMode.includes(AUTH_MODES.PASSWORD) && !hasPassword) {
+						return badRequestResponse('PASSWORD_REQUIRED')
+					}
+
+					if (type === common.USER_NAME) {
+						return badRequestResponse('EMAIL_OR_PHONE_REQUIRED')
+					}
+
+					const registrationBody = {
+						// name: bodyData.name || '',
+						password: bodyData.password,
+						otp: bodyData.otp,
+						_otpPurpose: OTP_PURPOSES.SIGNUP,
+					}
+
+					if (type === common.EMAIL) {
+						registrationBody.email = identifier
+					} else {
+						registrationBody.phone = identifier
+						registrationBody.phone_code = bodyData.phone_code
+					}
+
+					return await AccountHelper.create(registrationBody, deviceInformation, domain)
+				}
+
+				return badRequestResponse('IDENTIFIER_OR_PASSWORD_INVALID')
 			}
 
 			// Check active session limit
 			if (process.env.ALLOWED_ACTIVE_SESSIONS != null) {
 				const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id)
 				if (activeSessionCount >= process.env.ALLOWED_ACTIVE_SESSIONS) {
-					return responses.failureResponse({
-						message: 'ACTIVE_SESSION_LIMIT_EXCEEDED',
-						statusCode: httpStatusCode.not_acceptable,
-						responseCode: 'CLIENT_ERROR',
-					})
+					return badRequestResponse('ACTIVE_SESSION_LIMIT_EXCEEDED')
 				}
 			}
 
 			// Verify password
-			const isPasswordCorrect = await bcryptJs.compare(bodyData.password, user.password)
-			if (!isPasswordCorrect) {
-				return responses.failureResponse({
-					message: 'IDENTIFIER_OR_PASSWORD_INVALID',
-					statusCode: httpStatusCode.bad_request,
-					responseCode: 'CLIENT_ERROR',
-				})
+			let isPasswordCorrect = false
+			if (hasPassword) {
+				isPasswordCorrect = user.password ? await bcryptJs.compare(bodyData.password, user.password) : false
+			}
+
+			if (!isOtpValid && !isPasswordCorrect) {
+				return badRequestResponse('IDENTIFIER_OR_PASSWORD_INVALID')
 			}
 
 			// Create user session
@@ -830,6 +950,19 @@ module.exports = class AccountHelper {
 				refreshToken
 			)
 
+			if (hasOtp) {
+				const identifiers = [
+					redisIdentifier,
+					user.email,
+					user.username,
+					user.phone && `${bodyData.phone_code}${user.phone}`,
+				].filter(Boolean)
+
+				await Promise.allSettled(
+					identifiers.map((identifier) => utilsHelper.redisDel(otpRedisKey(purpose, identifier)))
+				)
+			}
+
 			return responses.successResponse({
 				statusCode: httpStatusCode.ok,
 				message: 'LOGGED_IN_SUCCESSFULLY',
@@ -1026,6 +1159,7 @@ module.exports = class AccountHelper {
 			}
 
 			// Helper functions to detect identifier type
+			// #TODO: Making this check from using the helper function
 			const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
 			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed
 			const isUsername = (str) => /^[a-zA-Z0-9_-]{3,30}$/.test(str)
@@ -1150,7 +1284,11 @@ module.exports = class AccountHelper {
 		}
 
 		// Validate that at least one contact method is provided
-		if (!bodyData.email && !bodyData.phone) {
+		const config = tenantDetail.configuration || {}
+		const allowedAuthMode = Array.isArray(config.allowed_auth_mode) ? config.allowed_auth_mode : []
+		const username = bodyData.username?.toLowerCase()
+
+		if (!bodyData.email && !bodyData.phone && !username) {
 			return responses.failureResponse({
 				message: 'EMAIL_OR_PHONE_REQUIRED',
 				statusCode: httpStatusCode.bad_request,
@@ -1181,92 +1319,128 @@ module.exports = class AccountHelper {
 		// Process email information
 		let encryptedEmailId = null
 		let plaintextEmailId = null
-		if (bodyData.email) {
-			plaintextEmailId = bodyData.email.toLowerCase()
-			encryptedEmailId = emailEncryption.encrypt(plaintextEmailId)
-			bodyData.email = encryptedEmailId
-		}
-
-		// Process phone information
 		let encryptedPhoneNumber = null
 		let plaintextPhoneNumber = null
-		if (bodyData.phone && bodyData.phone_code) {
-			plaintextPhoneNumber = bodyData.phone
-			encryptedPhoneNumber = emailEncryption.encrypt(plaintextPhoneNumber)
-			bodyData.phone = encryptedPhoneNumber
-		}
-
-		// Check if user already exists with email or phone or username
-		const criteria = []
-		if (encryptedEmailId) criteria.push({ email: encryptedEmailId })
-		if (encryptedPhoneNumber) criteria.push({ phone: encryptedPhoneNumber })
-		if (bodyData.username) criteria.push({ username: bodyData.username })
+		let phoneCode = bodyData.phone_code
+		let notificationName = bodyData.name
+		let redisIdentifiers = []
+		let purpose = OTP_PURPOSES.SIGNUP
 
-		if (criteria.length === 0) {
-			return // Skip if no criteria
-		}
+		if (username) {
+			const user = await userQueries.findOne(
+				{
+					username,
+					tenant_code: tenantDetail.code,
+					status: common.ACTIVE_STATUS,
+				},
+				{
+					attributes: ['email', 'phone', 'phone_code', 'name', 'username'],
+				}
+			)
 
-		// Check if user already exists with email or phone or username
-		let user = await userQueries.findOne(
-			{
-				[Op.or]: criteria,
-				password: { [Op.ne]: null },
-				tenant_code: tenantDetail.code,
-			},
-			{
-				attributes: ['id'],
+			if (!user) {
+				return responses.failureResponse({
+					message: 'ACCOUNT_NOT_FOUND',
+					statusCode: httpStatusCode.not_found,
+					responseCode: 'CLIENT_ERROR',
+				})
 			}
-		)
 
-		// Return error if user already exists
-		if (user) {
-			return responses.failureResponse({
-				message: 'USER_ALREADY_EXISTS',
-				statusCode: httpStatusCode.not_acceptable,
-				responseCode: 'CLIENT_ERROR',
-			})
-		}
+			purpose = OTP_PURPOSES.LOGIN
 
-		// Generate or retrieve OTP
-		let emailUserData = encryptedEmailId ? await utilsHelper.redisGet(encryptedEmailId) : null
-		let phoneUserData =
-			encryptedPhoneNumber && bodyData.phone_code
-				? await utilsHelper.redisGet(bodyData.phone_code + encryptedPhoneNumber)
-				: null
+			if (!allowedAuthMode.includes(AUTH_MODES.OTP)) {
+				return responses.failureResponse({
+					message: 'AUTH_MODE_NOT_ALLOWED',
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
+			}
 
-		// Use existing OTP if already in progress, otherwise generate a new one
-		const userData = emailUserData || phoneUserData
-		// Usage: [generateSecureOTP(), true]
+			encryptedEmailId = user.email || null
+			encryptedPhoneNumber = user.phone || null
+			phoneCode = user.phone_code || null
+			plaintextEmailId = encryptedEmailId ? emailEncryption.decrypt(encryptedEmailId) : null
+			plaintextPhoneNumber = encryptedPhoneNumber ? emailEncryption.decrypt(encryptedPhoneNumber) : null
+			notificationName = bodyData.name || user.name || 'User'
 
-		const [otp, isNew] =
-			userData && userData.action === 'signup' ? [userData.otp, false] : [utils.generateSecureOTP(), true]
+			if (!plaintextEmailId && !plaintextPhoneNumber) {
+				return responses.failureResponse({
+					message: 'USER_CONTACT_NOT_AVAILABLE',
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
+			}
 
-		// Store OTP data in redis if it's new
-		if (isNew) {
-			const redisData = {
-				verify: encryptedEmailId || encryptedPhoneNumber,
-				action: 'signup',
-				otp,
+			redisIdentifiers = [username]
+			if (encryptedEmailId) redisIdentifiers.push(encryptedEmailId)
+			if (encryptedPhoneNumber && phoneCode) redisIdentifiers.push(phoneCode + encryptedPhoneNumber)
+		} else {
+			if (bodyData.email) {
+				plaintextEmailId = bodyData.email.toLowerCase()
+				encryptedEmailId = emailEncryption.encrypt(plaintextEmailId)
+			}
+
+			if (bodyData.phone && bodyData.phone_code) {
+				plaintextPhoneNumber = bodyData.phone
+				encryptedPhoneNumber = emailEncryption.encrypt(plaintextPhoneNumber)
 			}
 
-			let redisSetResults = []
+			const criteria = []
+			if (encryptedEmailId) criteria.push({ email: encryptedEmailId })
+			if (encryptedPhoneNumber) criteria.push({ phone: encryptedPhoneNumber })
+
+			const existingUser =
+				criteria.length > 0
+					? await userQueries.findOne(
+							{
+								[Op.or]: criteria,
+								tenant_code: tenantDetail.code,
+							},
+							{ attributes: ['id'] }
+					  )
+					: null
+
+			const userExists = Boolean(existingUser)
+			purpose = resolveOtpPurpose({
+				username: null,
+				registrationCode: bodyData.registration_code,
+				userExists,
+			})
 
-			// Store OTP for email if provided
-			if (encryptedEmailId) {
-				const emailResult = await utilsHelper.redisSet(encryptedEmailId, redisData, common.otpExpirationTime)
-				redisSetResults.push(emailResult)
+			if (bodyData.registration_code && userExists) {
+				return responses.failureResponse({
+					message: 'USER_ALREADY_EXISTS',
+					statusCode: httpStatusCode.not_acceptable,
+					responseCode: 'CLIENT_ERROR',
+				})
 			}
 
-			// Store OTP for phone if provided
-			if (encryptedPhoneNumber && bodyData.phone_code) {
-				const phoneResult = await utilsHelper.redisSet(
-					bodyData.phone_code + encryptedPhoneNumber,
-					redisData,
-					common.otpExpirationTime
-				)
-				redisSetResults.push(phoneResult)
+			if (purpose === OTP_PURPOSES.LOGIN && !allowedAuthMode.includes(AUTH_MODES.OTP)) {
+				return responses.failureResponse({
+					message: 'AUTH_MODE_NOT_ALLOWED',
+					statusCode: httpStatusCode.bad_request,
+					responseCode: 'CLIENT_ERROR',
+				})
 			}
 
+			if (encryptedEmailId) redisIdentifiers.push(encryptedEmailId)
+			if (encryptedPhoneNumber && phoneCode) redisIdentifiers.push(phoneCode + encryptedPhoneNumber)
+		}
+
+		const existingOtpData = await getExistingOtpForPurpose(purpose, redisIdentifiers)
+
+		const otp = existingOtpData ? existingOtpData.otp : utils.generateSecureOTP()
+		const isNew = !existingOtpData
+
+		const redisData = {
+			verify: username || encryptedEmailId || encryptedPhoneNumber,
+			action: purpose,
+			otp,
+		}
+
+		if (isNew) {
+			const redisSetResults = await setOtpForPurpose(purpose, redisIdentifiers, redisData)
+
 			// Check if storing in Redis was successful
 			if (!redisSetResults.includes('OK')) {
 				return responses.failureResponse({
@@ -1282,14 +1456,14 @@ module.exports = class AccountHelper {
 			notificationUtils.sendEmailNotification({
 				emailId: plaintextEmailId,
 				templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE,
-				variables: { name: bodyData.name || plaintextEmailId, otp },
+				variables: { name: notificationName || plaintextEmailId, otp },
 				tenantCode: tenantDetail.code,
 				organization_code: process.env.DEFAULT_ORGANISATION_CODE || null,
 			})
 		}
 
 		// Send SMS notification with OTP if phone is provided
-		if (plaintextPhoneNumber && bodyData.phone_code) {
+		if (plaintextPhoneNumber && (bodyData.phone_code || phoneCode)) {
 			notificationUtils.sendSMSNotification({
 				phoneNumber: plaintextPhoneNumber,
 				templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE,
@@ -1355,6 +1529,7 @@ module.exports = class AccountHelper {
 			}
 
 			// Helper functions to detect identifier type
+			// #TODO: Making this check from using the helper function
 			const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
 			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed
 			const isUsername = (str) => /^[a-zA-Z0-9_-]{3,30}$/.test(str)
diff --git a/src/services/admin.js b/src/services/admin.js
index 13fc47a62..7e777b0cd 100644
--- a/src/services/admin.js
+++ b/src/services/admin.js
@@ -438,10 +438,7 @@ module.exports = class AdminHelper {
 			const identifier = rawIdentifier.toLowerCase()
 			if (!identifier) return failure('IDENTIFIER_REQUIRED', httpStatusCode.bad_request)
 
-			// identifier type helpers
-			const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
-			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str)
-			const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str)
+			const identifierType = utils.checkIdentifierType(identifier)
 
 			// 2) Build query (skip domain checks; use DEFAULT_TENANT_CODE)
 			const query = {
@@ -451,9 +448,9 @@ module.exports = class AdminHelper {
 				tenant_code: process.env.DEFAULT_TENANT_CODE,
 			}
 
-			if (isEmail(identifier)) {
+			if (identifierType === common.EMAIL) {
 				query[Op.or].push({ email: emailEncryption.encrypt(identifier) })
-			} else if (isPhone(identifier)) {
+			} else if (identifierType === common.PHONE) {
 				// expects bodyData.phone_code when phone login is used
 				query[Op.or].push({
 					phone: emailEncryption.encrypt(identifier),
@@ -571,11 +568,6 @@ module.exports = class AdminHelper {
 	 */
 	static async addOrgAdmin(userId, organizationId, loggedInUserId, identifier, tenantCode, phoneCode) {
 		try {
-			// Helper functions for identifier validation
-			const isEmail = (str) => /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str)
-			const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str)
-			const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str)
-
 			let user
 
 			// Try finding user by userId if provided
@@ -595,6 +587,7 @@ module.exports = class AdminHelper {
 			// If user not found by userId, try finding by identifier
 			if (!user?.id && identifier?.trim()) {
 				const normalizedIdentifier = identifier.trim().toLowerCase()
+				const identifierType = utils.checkIdentifierType(normalizedIdentifier)
 				const query = {
 					[Op.or]: [],
 					tenant_code: tenantCode,
@@ -602,9 +595,9 @@ module.exports = class AdminHelper {
 					password: { [Op.ne]: null },
 				}
 
-				if (isEmail(normalizedIdentifier)) {
+				if (identifierType === common.EMAIL) {
 					query[Op.or].push({ email: emailEncryption.encrypt(normalizedIdentifier) })
-				} else if (isPhone(normalizedIdentifier)) {
+				} else if (identifierType === common.PHONE) {
 					if (!phoneCode) {
 						return responses.failureResponse({
 							message: 'PHONE_CODE_REQUIRED',
@@ -613,7 +606,7 @@ module.exports = class AdminHelper {
 						})
 					}
 					query[Op.or].push({ phone: emailEncryption.encrypt(normalizedIdentifier), phone_code: phoneCode })
-				} else if (isUsername(normalizedIdentifier)) {
+				} else if (identifierType === common.USER_NAME) {
 					query[Op.or].push({ username: normalizedIdentifier })
 				} else {
 					return responses.failureResponse({
diff --git a/src/services/tenant.js b/src/services/tenant.js
index 8aa85fe41..6bc3b29f8 100644
--- a/src/services/tenant.js
+++ b/src/services/tenant.js
@@ -50,7 +50,8 @@ module.exports = class tenantHelper {
 
 			// pick relevant data from body data for tenant creation
 			const tenantCreateBody = {
-				..._.pick(bodyData, ['name', 'code', 'description', 'logo', 'theming', 'meta']),
+				..._.pick(bodyData, ['name', 'code', 'description', 'logo', 'theming', 'configuration', 'meta']),
+				configuration: bodyData.configuration || _.cloneDeep(common.DEFAULT_TENANT_CONFIGURATION),
 				status: common.ACTIVE_STATUS,
 				created_by: userId,
 				updated_by: userId,
@@ -393,6 +394,7 @@ module.exports = class tenantHelper {
 					created_at: tenantCreateResponse?.created_at || new Date(),
 					updated_at: tenantCreateResponse?.updated_at || new Date(),
 					meta: tenantCreateResponse?.meta || {},
+					configuration: tenantCreateResponse?.configuration,
 					status: tenantCreateResponse?.status || common.ACTIVE_STATUS,
 					deleted: false,
 					org_id: tenantCreateResponse.orgId,
@@ -729,7 +731,7 @@ module.exports = class tenantHelper {
 			if (isAdmin) {
 				options.organizationAttributes = ['id', 'name', 'code']
 			} else {
-				options.attributes = ['code', 'name', 'description', 'meta']
+				options.attributes = ['code', 'name', 'description', 'configuration', 'meta']
 			}
 
 			// fetch tenant details
diff --git a/src/utils/usernameGenerator.js b/src/utils/usernameGenerator.js
index d495a21a9..f6bbcf872 100644
--- a/src/utils/usernameGenerator.js
+++ b/src/utils/usernameGenerator.js
@@ -18,14 +18,13 @@ async function generateUniqueUsername(name) {
 
 	// Sanitize the name: lowercase, remove spaces, and special characters
 	const sanitizedName = name
-		.toLowerCase()
+		?.toLowerCase()
 		.replace(/[^a-z0-9]/g, '')
 		.slice(0, 10) // Trim to keep username short
-
+	const prefix = sanitizedName || cachedNanoid(10).toLowerCase()
 	// Generate a shorter random suffix (14 characters)
 	const randomSuffix = cachedNanoid(14).toLowerCase() // Ensure suffix is also lowercase
-
-	return `${sanitizedName}_${randomSuffix}`
+	return `${prefix}_${randomSuffix}`
 }
 
 async function generateUniqueCodeString(stringLength = 4) {
diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js
index 04badf548..713ea7663 100644
--- a/src/validators/v1/account.js
+++ b/src/validators/v1/account.js
@@ -39,9 +39,8 @@ module.exports = {
 
 		// Validate name
 		req.checkBody('name')
+			.optional({ checkFalsy: true })
 			.trim()
-			.notEmpty()
-			.withMessage('name field is empty')
 			.matches(/^[A-Za-z ]+$/)
 			.withMessage('This field can only contain alphabets')
 
@@ -78,13 +77,19 @@ module.exports = {
 
 		// Validate password
 		req.checkBody('password')
-			.notEmpty()
-			.withMessage('Password field is empty')
+			.optional({ checkFalsy: true })
 			.matches(process.env.PASSWORD_POLICY_REGEX)
 			.withMessage(process.env.PASSWORD_POLICY_MESSAGE)
 			.custom((value) => !/\s/.test(value))
 			.withMessage('Password cannot contain spaces')
 
+		req.checkBody('otp')
+			.optional({ checkFalsy: true })
+			.matches(/^[0-9]+$/)
+			.withMessage('Otp should be number')
+			.isLength({ min: 6, max: 6 })
+			.withMessage('Otp is invalid')
+
 		// Validate role if provided
 		if (req.body.role) {
 			req.checkBody('role').trim().not().isIn([common.ADMIN_ROLE]).withMessage("User doesn't have admin access")
@@ -138,7 +143,20 @@ module.exports = {
 			.withMessage('Phone code must be a valid country code (e.g., +1, +91)')
 
 		// Validate password
-		req.checkBody('password').trim().notEmpty().withMessage('Password field is empty')
+		req.checkBody('password')
+			.optional({ checkFalsy: true })
+			.trim()
+			.matches(process.env.PASSWORD_POLICY_REGEX)
+			.withMessage(process.env.PASSWORD_POLICY_MESSAGE)
+			.custom((value) => !/\s/.test(value))
+			.withMessage('Password cannot contain spaces')
+
+		req.checkBody('otp')
+			.optional({ checkFalsy: true })
+			.matches(/^[0-9]+$/)
+			.withMessage('otp should be number')
+			.isLength({ min: 6, max: 6 })
+			.withMessage('Otp is invalid')
 
 		return req
 	},
@@ -161,6 +179,8 @@ module.exports = {
 
 	registrationOtp: (req) => {
 		req.body = filterRequestBody(req.body, account.registrationOtp)
+		req.body.username = req.body.username ? req.body.username.toLowerCase() : req.body.username
+
 		// Validate email (optional)
 		req.checkBody('email')
 			.optional()
@@ -178,6 +198,13 @@ module.exports = {
 			.trim()
 			.matches(/^[0-9]{7,15}$/)
 			.withMessage('phone must be a valid number between 7 and 15 digits')
+
+		req.checkBody('username')
+			.optional()
+			.trim()
+			.matches(/^[a-zA-Z0-9_-]{3,30}$/)
+			.withMessage('username is invalid')
+
 		// Validate registration_code
 		req.checkBody('registration_code').trim().withMessage('registration_code cannot be empty.')
 
@@ -188,13 +215,14 @@ module.exports = {
 			.matches(/^\+[1-9][0-9]{0,3}$/)
 			.withMessage('phone_code must be a valid country code (e.g., +1, +91)')
 
-		req.checkBody(['email', 'phone', 'phone_code']).custom(() => {
+		req.checkBody(['email', 'phone', 'phone_code', 'username']).custom(() => {
 			const phone = req.body.phone
 			const phone_code = req.body.phone_code
 			const email = req.body.email
+			const username = req.body.username
 
-			if (!email && !phone) {
-				throw new Error('At least one of email or phone must be provided')
+			if (!email && !phone && !username) {
+				throw new Error('At least one of email, phone, or username must be provided')
 			}
 
 			if (phone && !phone_code) {
@@ -204,7 +232,7 @@ module.exports = {
 			return true
 		})
 
-		req.checkBody('name').notEmpty().withMessage('name field is empty')
+		req.checkBody('name').optional({ checkFalsy: true }).withMessage('name field is empty')
 	},
 
 	resetPassword: (req) => {
diff --git a/src/validators/v1/tenant.js b/src/validators/v1/tenant.js
index 2a448edf4..8883001c2 100644
--- a/src/validators/v1/tenant.js
+++ b/src/validators/v1/tenant.js
@@ -8,18 +8,70 @@ const common = require('@constants/common')
 const filterRequestBody = require('../common')
 const { tenant } = require('@constants/blacklistConfig')
 
+const parseObject = (value) => {
+	if (typeof value === 'string') {
+		try {
+			return JSON.parse(value)
+		} catch (e) {
+			return false
+		}
+	}
+
+	return value
+}
+
+const isPlainObject = (value) => {
+	return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+const isValidObject = (value) => {
+	return isPlainObject(parseObject(value))
+}
+
+const isValidTenantConfiguration = (value) => {
+	const configuration = parseObject(value)
+	const allowedAuthModes = common.DEFAULT_TENANT_CONFIGURATION.allowed_auth_mode
+
+	return (
+		isPlainObject(configuration) &&
+		Array.isArray(configuration.allowed_auth_mode) &&
+		configuration.allowed_auth_mode.length > 0 &&
+		configuration.allowed_auth_mode.every((authMode) => allowedAuthModes.includes(authMode)) &&
+		typeof configuration.auto_register === 'boolean'
+	)
+}
+
+const normalizeTenantConfiguration = (req) => {
+	if (req.body.configuration === '') {
+		delete req.body.configuration
+		return
+	}
+
+	if (typeof req.body.configuration === 'string') {
+		const configuration = parseObject(req.body.configuration)
+		if (configuration) req.body.configuration = configuration
+	}
+}
+
 module.exports = {
 	update: (req) => {
 		if (req.params.id) {
 			req.body = filterRequestBody(req.body, tenant.update)
+			normalizeTenantConfiguration(req)
 			req.checkParams('id')
 				.trim()
 				.notEmpty()
 				.withMessage('code param is empty')
 				.matches(/^[a-zA-Z0-9_]+$/)
 				.withMessage('Code must contain only letters, numbers, and underscores')
+
+			req.checkBody('configuration')
+				.optional({ checkFalsy: true })
+				.custom(isValidTenantConfiguration)
+				.withMessage('Configuration must include allowed_auth_mode, and auto_register')
 		} else {
 			req.body = filterRequestBody(req.body, tenant.create)
+			normalizeTenantConfiguration(req)
 
 			req.checkBody('name').trim().notEmpty().withMessage('name field is empty')
 
@@ -36,36 +88,17 @@ module.exports = {
 
 			req.checkBody('theming')
 				.optional({ checkFalsy: true })
-				.custom((value) => {
-					// If value is a string, try parsing it as JSON
-					if (typeof value === 'string') {
-						try {
-							const parsed = JSON.parse(value)
-							return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
-						} catch (e) {
-							return false
-						}
-					}
-					// If value is not a string, check if it’s a plain object
-					return typeof value === 'object' && value !== null && !Array.isArray(value)
-				})
+				.custom(isValidObject)
 				.withMessage('Theming must be a valid object or a JSON string representing an object')
 
+			req.checkBody('configuration')
+				.optional({ checkFalsy: true })
+				.custom(isValidTenantConfiguration)
+				.withMessage('Configuration must include allowed_auth_mode, and auto_register')
+
 			req.checkBody('meta')
 				.optional({ checkFalsy: true })
-				.custom((value) => {
-					// If value is a string, try parsing it as JSON
-					if (typeof value === 'string') {
-						try {
-							const parsed = JSON.parse(value)
-							return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
-						} catch (e) {
-							return false
-						}
-					}
-					// If value is not a string, check if it’s a plain object
-					return typeof value === 'object' && value !== null && !Array.isArray(value)
-				})
+				.custom(isValidObject)
 				.withMessage('Meta must be a valid object or a JSON string representing an object')
 
 			req.checkBody('domains').trim().notEmpty().withMessage('domains field is empty')
diff --git a/src/validators/v1/user.js b/src/validators/v1/user.js
index 0e2575265..2b6036919 100644
--- a/src/validators/v1/user.js
+++ b/src/validators/v1/user.js
@@ -9,12 +9,14 @@ const { user } = require('@constants/blacklistConfig')
 module.exports = {
 	update: (req) => {
 		req.checkBody('name')
+			.optional()
 			.trim()
 			.notEmpty()
 			.withMessage('name field is empty')
 			.matches(/^[A-Za-z ]+$/)
 			.withMessage('This field can only contain alphabets')
 		req.checkBody('about')
+			.optional()
 			.trim()
 			.notEmpty()
 			.withMessage('about field is empty')
