diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 489fd6e0da7c19871df14f43689ab3bb1fa1518d..893b08d7872a9b89be860f0b7af54c1ccd446724 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -10,6 +10,7 @@ class Organization < ApplicationRecord
 
     has_many :namespaces
     has_many :groups
+    has_many :projects
 
     has_one :settings, class_name: "OrganizationSetting"
 
diff --git a/app/models/project.rb b/app/models/project.rb
index cb1468a8d0c76277908b458466a46a94f452dcaf..4e69cdeff13326ccde18ed7a849a45e255e3d13e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -165,6 +165,7 @@ class Project < ApplicationRecord
   # Relations
   belongs_to :pool_repository
   belongs_to :creator, class_name: 'User'
+  belongs_to :organization, class_name: 'Organizations::Organization'
   belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id'
   belongs_to :namespace
   # Sync deletion via DB Trigger to ensure we do not have
diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml
index 6bd6f99a1344979567174bf7080c3b3d9ca366f7..c12a2dfda7bf5b32c17a2b75445767500c7fea13 100644
--- a/config/gitlab_loose_foreign_keys.yml
+++ b/config/gitlab_loose_foreign_keys.yml
@@ -287,6 +287,10 @@ pages_deployments:
   - table: p_ci_builds
     column: ci_build_id
     on_delete: async_nullify
+projects:
+  - table: organizations
+    column: organization_id
+    on_delete: async_nullify
 requirements_management_test_reports:
   - table: ci_builds
     column: build_id
diff --git a/db/migrate/20230822064649_add_organization_id_to_project.rb b/db/migrate/20230822064649_add_organization_id_to_project.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9607f711981b3302178ee9140d620fdde70667d5
--- /dev/null
+++ b/db/migrate/20230822064649_add_organization_id_to_project.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddOrganizationIdToProject < Gitlab::Database::Migration[2.1]
+  DEFAULT_ORGANIZATION_ID = 1
+
+  enable_lock_retries!
+
+  def change
+    add_column :projects, :organization_id, :bigint, default: DEFAULT_ORGANIZATION_ID, null: true # rubocop:disable Migration/AddColumnsToWideTables
+  end
+end
diff --git a/db/post_migrate/20230822064841_prepare_index_for_org_id_on_projects.rb b/db/post_migrate/20230822064841_prepare_index_for_org_id_on_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f822a440a40677bb71e272856e5b4cb41484e97
--- /dev/null
+++ b/db/post_migrate/20230822064841_prepare_index_for_org_id_on_projects.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class PrepareIndexForOrgIdOnProjects < Gitlab::Database::Migration[2.1]
+  INDEX_NAME = 'index_projects_on_organization_id'
+
+  def up
+    prepare_async_index :projects, :organization_id, name: INDEX_NAME
+  end
+
+  def down
+    unprepare_async_index :projects, :organization_id, name: INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20230822064649 b/db/schema_migrations/20230822064649
new file mode 100644
index 0000000000000000000000000000000000000000..449dd984431776a00363c982875ae221ff5ef7a0
--- /dev/null
+++ b/db/schema_migrations/20230822064649
@@ -0,0 +1 @@
+b892940441125e854d08e24906e4b6287f8359b4ad374be5b141b43cfdcc1354
\ No newline at end of file
diff --git a/db/schema_migrations/20230822064841 b/db/schema_migrations/20230822064841
new file mode 100644
index 0000000000000000000000000000000000000000..2922af9c57334991bf8adf1392e5cd247e5f13f2
--- /dev/null
+++ b/db/schema_migrations/20230822064841
@@ -0,0 +1 @@
+e025eb64ab8b9ece1a18c845024db272a1859757734948609c134f6dfee93884
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9c1fc3b5500c3aa5b0f8b844d4d85427b4205536..805745d44709ccabbd0dc64ecb58e74a8073c7e4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -21836,7 +21836,8 @@ CREATE TABLE projects (
     autoclose_referenced_issues boolean,
     suggestion_commit_message character varying(255),
     project_namespace_id bigint,
-    hidden boolean DEFAULT false NOT NULL
+    hidden boolean DEFAULT false NOT NULL,
+    organization_id bigint DEFAULT 1
 );
 
 CREATE SEQUENCE projects_id_seq
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 04a83a1f6ab7d4020d9587b0f638804a9563580e..fea3c5b77a130174384eb8dabf06db7fd40eca97 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -15,7 +15,8 @@
     search_namespace_index_assignments: [%w[search_index_id index_type]],
     slack_integrations_scopes: [%w[slack_api_scope_id]],
     notes: %w[namespace_id], # this index is added in an async manner, hence it needs to be ignored in the first phase.
-    vulnerabilities: [%w[finding_id]] # index will be created in https://gitlab.com/gitlab-org/gitlab/-/issues/423541
+    vulnerabilities: [%w[finding_id]], # index will be created in https://gitlab.com/gitlab-org/gitlab/-/issues/423541
+    projects: %w[organization_id] # this index is added in an async manner, hence it needs to be ignored in the first phase.
   }.with_indifferent_access.freeze
 
   TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 99e871e5255693f6b2725c25076cac14109a2d7a..de3f28a5b90641d311be5b101f4f4b25b841d2f4 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -823,6 +823,7 @@ project:
 - project_state
 - security_policy_bots
 - target_branch_rules
+- organization
 award_emoji:
 - awardable
 - user
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index 7838fc1c5a4fceb4a6a1dc0fed2f635f22575d67..2f9f04fd3e64546fa35c23c89528bb7e0794a3e2 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -11,6 +11,7 @@
     it { is_expected.to have_many :groups }
     it { is_expected.to have_many(:users).through(:organization_users).inverse_of(:organizations) }
     it { is_expected.to have_many(:organization_users).inverse_of(:organization) }
+    it { is_expected.to have_many :projects }
   end
 
   describe 'validations' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 657c7d5dee8bbd1fa518aaae82d4bdc039b96fd5..5312ca0ef7024b805ada755f6a62c006f16cb2ac 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -17,6 +17,7 @@
   it_behaves_like 'ensures runners_token is prefixed', :project
 
   describe 'associations' do
+    it { is_expected.to belong_to(:organization) }
     it { is_expected.to belong_to(:group) }
     it { is_expected.to belong_to(:namespace) }
     it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id').inverse_of(:project) }
@@ -9189,6 +9190,13 @@ def create_hook
     end
   end
 
+  context 'with loose foreign key on organization_id' do
+    it_behaves_like 'cleanup by a loose foreign key' do
+      let_it_be(:parent) { create(:organization) }
+      let_it_be(:model) { create(:project, organization: parent) }
+    end
+  end
+
   private
 
   def finish_job(export_job)
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 1055c1a6968620166345a7bf66e4b1d24ccab570..677cb243a7cab02f383a0fd89f0d9ed85b1d98c1 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -44,6 +44,7 @@ itself: # project
     - storage_version
     - topic_list
     - verification_checksum
+    - organization_id
   remapped_attributes:
     avatar: avatar_url
     build_allow_git_fetch: build_git_strategy