diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 0a76f5bcc58277d946c295b2efc00a273c0b0572..0000000000000000000000000000000000000000
--- a/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/build
-/bin
-/.idea
-/.gradle
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 99347f14d395ba52d0d6d5787e08f73d8ff00ba6..0000000000000000000000000000000000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-package:
-  image:
-    name: gradle:jdk17
-  script:
-    - gradle jar shadowjar
-  artifacts:
-    paths:
-      - build/libs/keycloak-scim-1.0-SNAPSHOT.jar
-      - build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar
-      - build/libs/keycloak-scim-1.0-SNAPSHOT-all-legacy.jar
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index e0f15db2eb22b5d618150277e48b741f8fdd277a..0000000000000000000000000000000000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "java.configuration.updateBuildConfiguration": "automatic"
-}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index ff4ec16caeffb4425eb9695701fd696d04c415b6..0000000000000000000000000000000000000000
--- a/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright 2022 libre.sh / scim
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/README.md b/README.md
index 785758b00710b8755947fe56ce4d369bd736ffdf..766de55fe98ee5b86148d98f4abddb902378cd09 100644
--- a/README.md
+++ b/README.md
@@ -1,104 +1 @@
-# keycloak-scim-client
-
-This extension add [SCIM2](http://www.simplecloud.info) client capabilities to Keycloak.
-
-It allows to :
-
-* Declare SCIM endpoints (through the identity federation UI). Any tool implementing SCIM protocol can be wired to the
-  Keycloack instance through this declaration.
-* Propagate users and groups from Keycloack to SCIM endpoints : when a user/group gets created or modified in Keycloack,
-  the modification is fowarded to all declared SCIM endpoints through SCIM calls within the transaction scope. If
-  propagation fails, changes can be rolled back or not according to a configurable rollback strategy.
-* Import users and groups from SCIM endpoints (through the Keycloack synchronization mechanism).
-
-See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643)
-and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details
-
-## Overview
-
-### Motivation
-
-We want to build a unified collaborative platform based on multiple applications. To do that, we need a way to propagate
-immediately changes made in Keycloak to all these applications. And we want to keep using OIDC or SAML as the
-authentication protocol.
-
-This will allow users to collaborate seamlessly across the platform without requiring every user to have connected once
-to each application. This will also ease GDRP compliance because deleting a user in Keycloak will delete the user from
-every app. The SCIM protocol is standard, comprehensible and easy to implement. It's a perfect fit for our goal.
-
-We chose to build application extensions/plugins because it's easier to deploy and thus will benefit to a larger portion
-of the FOSS community.
-
-#### Keycloak specific
-
-This extension uses 3 concepts in KeyCloack :
-
-- Event Listener : used to listen for changes within Keycloack (e.g. User creation, Group deletion...) and propagate
-  them to registered SCIM service providers through SCIM requests.
-- Federation Provider : used to set up all the SCIM service providers endpoint without creating our own UI.
-- JPA Entity Provider : used to save the mapping between the local IDs and the service providers IDs.
-
-## Usage
-
-### Development mode
-
-From the repository root :
-
-* Launch the docker-compose image (composed of a postgre and keycloack instance runing on localhost:8080) :
-  ``docker compose up -d``
-* Execute ``gradle jar shadowJar && docker compose restart keycloak`` to build extension and update the Keycloack
-  instance
-* You can access extension logs through ``docker compose logs -f``
-
-### Installation
-
-1. Download
-   the [latest version](https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package)
-2. Put it in `/opt/keycloak/providers/`.
-
-It's also possible to build your own custom image if you run Keycloak in a [container](/docs/container.md).
-
-Other [installation options](/docs/installation.md) are available.
-
-### Setup
-
-#### Enable SCIM Event listeners
-
-1. Go to `Admin Console > Events > Config`.
-2. Add `scim` in `Event Listeners`.
-3. Save.
-
-![Event listener page](/docs/img/event-listener-page.png)
-
-#### Register SCIM Service Providers
-
-1. Go to `Admin Console > Realm Settings > Events`.
-2. Add `scim` to the list of event listers
-3. Save
-
-![Federation provider page](/docs/img/federation-provider-page.png)
-
-### Configuration
-
-Add the endpoint - for a local set up you have to add the two containers in a docker network and use the container ip
-see [here](https://docs.docker.com/engine/reference/commandline/network/)
-If you use the [rocketchat app](https://lab.libreho.st/libre.sh/scim/rocketchat-scim) you get the endpoint from your
-rocket Chat Scim Adapter App Details.
-Endpoint content type is application/json.
-Auth mode Bearer or None for local test setup.
-Copy the bearer token from your app details in rocketchat.
-
-If you enable import during sync then you can choose between to following import actions:
-
-- Create Local - adds users to keycloak
-- Nothing
-- Delete Remote - deletes users from the remote application
-
-### Sync
-
-You can set up a periodic sync for all users or just changed users - it's not mandatory. You can either do:
-
-- Periodic Full Sync
-- Periodic Changed User Sync
-
-**[License AGPL](/LICENSE)**
+Moved to https://forge.libre.sh/libre.sh/keycloak-scim.
\ No newline at end of file
diff --git a/auto.sh b/auto.sh
deleted file mode 100755
index 884ba67840cdd501ca287f939176531aa76216f4..0000000000000000000000000000000000000000
--- a/auto.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-gradle jar shadowjar
-scp build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar root@192.168.130.252:/var/www/html/keycloak-scim-1.0-SNAPSHOT-all.jar
-scp build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar root@192.168.130.252:/var/www/html/keycloak-scim-aws-1.0-SNAPSHOT-all.jar
-k delete pod keycloak-keycloakx-0 -n keycloak
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 7897ff6a60548a0fa5baed04f395cd45a54af10a..0000000000000000000000000000000000000000
--- a/build.gradle
+++ /dev/null
@@ -1,28 +0,0 @@
-plugins {
-    id 'java'
-    id 'com.github.johnrengelman.shadow' version '8.1.1'
-    id "org.sonarqube" version "5.1.0.4882"
-    id "com.github.ben-manes.versions" version "0.51.0"
-}
-
-group = 'sh.libre.scim'
-version = '1.0-SNAPSHOT'
-description = 'keycloak-scim'
-
-java.sourceCompatibility = JavaVersion.VERSION_17
-
-repositories {
-    mavenLocal()
-    mavenCentral()
-}
-
-dependencies {
-    compileOnly 'org.keycloak:keycloak-core:26.0.1'
-    compileOnly 'org.keycloak:keycloak-server-spi:26.0.1'
-    compileOnly 'org.keycloak:keycloak-server-spi-private:26.0.1'
-    compileOnly 'org.keycloak:keycloak-services:26.0.1'
-    compileOnly 'org.keycloak:keycloak-model-jpa:26.0.1'
-    implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
-    implementation 'de.captaingoldfish:scim-sdk-common:1.26.0'
-    implementation 'de.captaingoldfish:scim-sdk-client:1.26.0'
-}
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 43e4f3a77023c85f6d4a0859eaf36b032e8b2b63..0000000000000000000000000000000000000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-version: "3"
-
-services:
-  postgres:
-    image: postgres
-    volumes:
-      - db:/var/lib/postgresql/data
-    environment:
-      POSTGRES_USER: keycloak
-      POSTGRES_PASSWORD: keycloak
-    ports:
-      - 5432:5432
-  keycloak:
-    image: quay.io/keycloak/keycloak:26.0.1
-    build: .
-    command: start-dev
-    volumes:
-      - ./build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar:/opt/keycloak/providers/keycloak-scim-1.0-SNAPSHOT-all.jar
-    environment:
-      KC_DB: postgres
-      KC_DB_URL_HOST: postgres
-      KC_DB_USERNAME: keycloak
-      KC_DB_PASSWORD: keycloak
-      KEYCLOAK_ADMIN: admin
-      KEYCLOAK_ADMIN_PASSWORD: admin
-      KC_LOG_LEVEL: INFO,sh.libre.scim:debug,de.captaingoldfish.scim:debug
-    ports:
-      - 127.0.0.1:8080:8080
-    depends_on:
-      - postgres
-
-volumes:
-  db:
diff --git a/docs/container.md b/docs/container.md
deleted file mode 100644
index 086b87f06b93726d638b73c9187a742763f73049..0000000000000000000000000000000000000000
--- a/docs/container.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Container
-
-## Quarkus
-
-```
-FROM quay.io/keycloak/keycloak as builder
-RUN curl "https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package" -Lo /opt/keycloak/providers/keycloak-scim-1.0-SNAPSHOT-all.jar
-RUN /opt/keycloak/bin/kc.sh build
-
-FROM quay.io/keycloak/keycloak
-COPY --from=builder /opt/keycloak/ /opt/keycloak/
-WORKDIR /opt/keycloak
-ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
-CMD ["start"]
-```
diff --git a/docs/dependencies.md b/docs/dependencies.md
deleted file mode 100644
index 424136e5dc3a172fb9126f303ea314906d745f31..0000000000000000000000000000000000000000
--- a/docs/dependencies.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Dependencies
-
-| Name | Version | Quarkus | Wildfly | Download |
-| --- | --- | --- | --- | --- |
-| io.github.resilience4j:resilience4j-retry | 1.7.1 | X | X | [link](https://repo1.maven.org/maven2/io/github/resilience4j/resilience4j-retry/1.7.1/resilience4j-retry-1.7.1.jar) |
-| io.github.resilience4j:resilience4j-core | 1.7.1 | X | X | [link](https://repo1.maven.org/maven2/io/github/resilience4j/resilience4j-core/1.7.1/resilience4j-core-1.7.1.jar) |
-| io.vavr:vavr | 0.10.2 | X | X | [link](https://repo1.maven.org/maven2/io/vavr/vavr/0.10.2/vavr-0.10.2.jar) |
-| io.vavr:vavr-match | 0.10.2 | X | X | [link](https://repo1.maven.org/maven2/io/vavr/vavr-match/0.10.2/vavr-match-0.10.2.jar) |
-| org.slf4j:slf4j-api | 1.7.30 | X | X | [link](https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar) |
-| com.unboundid.product.scim2:scim2-sdk-client | 2.3.7 | X | X | [link](https://repo1.maven.org/maven2/com/unboundid/product/scim2/scim2-sdk-client/2.3.7/scim2-sdk-client-2.3.7.jar) |
-| com.unboundid.product.scim2:scim2-sdk-common | 2.3.7 | X | X | [link](https://repo1.maven.org/maven2/com/unboundid/product/scim2/scim2-sdk-common/2.3.7/scim2-sdk-common-2.3.7.jar) |
-| org.wildfly.client:wildfly-client-config | 1.0.1.Final | X |  | [link](https://repo1.maven.org/maven2/org/wildfly/client/wildfly-client-config/1.0.1.Final/wildfly-client-config-1.0.1.Final.jar) |
-| org.jboss.resteasy:resteasy-client | 4.7.6.Final | X |  | [link](https://repo1.maven.org/maven2/org/jboss/resteasy/resteasy-client/4.7.6.Final/resteasy-client-4.7.6.Final.jar) |
-| org.jboss.resteasy:resteasy-client-api | 4.7.6.Final | X |  | [link](https://repo1.maven.org/maven2/org/jboss/resteasy/resteasy-client-api/4.7.6.Final/resteasy-client-api-4.7.6.Final.jar) |
\ No newline at end of file
diff --git a/docs/img/event-listener-page.png b/docs/img/event-listener-page.png
deleted file mode 100644
index 5c45734c68fcc9ecfe558cd84fb571c8fcffb20f..0000000000000000000000000000000000000000
Binary files a/docs/img/event-listener-page.png and /dev/null differ
diff --git a/docs/img/federation-provider-page.png b/docs/img/federation-provider-page.png
deleted file mode 100644
index 390128f581b6bca7ea04d272ab71299788a99f79..0000000000000000000000000000000000000000
Binary files a/docs/img/federation-provider-page.png and /dev/null differ
diff --git a/docs/installation.md b/docs/installation.md
deleted file mode 100644
index c79919c63b3c8ad4efdc512199e1cddd2679c452..0000000000000000000000000000000000000000
--- a/docs/installation.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Installation (advanced)
-
-## 1. Download the jar files
-
-### All-in-one
-
-This [package](<(https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package)>) contains the SCIM provider and it's dependencies. It's intended only for Quarkus distribution.
-
-### Separately
-
-You'll need the SCIM provider's [package](<(https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT.jar?job=package)>) and each of its [dependencies](dependencies.md).
-
-## 2. Install the jar files
-
-### Quarkus
-
-Copy the downloaded file in `/opt/keycloak/providers/`. For production use, build Keycloak before starting it.
-
-### Wildfly (legacy)
-
-Copy the downloaded file in `/opt/jboss/keycloak/standalone/deployments/`.
diff --git a/keycloak-scim.iml b/keycloak-scim.iml
deleted file mode 100644
index 414103a5e4ed1cdbe3f3b022db200dfb865d29b3..0000000000000000000000000000000000000000
--- a/keycloak-scim.iml
+++ /dev/null
@@ -1,106 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_11">
-    <output url="file://$MODULE_DIR$/target/classes" />
-    <output-test url="file://$MODULE_DIR$/target/test-classes" />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
-      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
-      <excludeFolder url="file://$MODULE_DIR$/target" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-core:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-common:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.bouncycastle:bcprov-jdk15on:1.68" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.bouncycastle:bcpkix-jdk15on:1.68" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-server-spi:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-server-spi-private:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.ua-parser:uap-java:1.4.3" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.yaml:snakeyaml:1.20" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.commons:commons-collections4:4.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-services:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.mail:jakarta.mail:1.6.5" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish:jakarta.json:1.1.6" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.twitter4j:twitter4j-core:4.0.7" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.1_spec:2.0.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:2.0.0.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20211018.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.zxing:javase:3.4.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.zxing:core:3.4.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.jai-imageio:jai-imageio-core:1.4.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.openshift:openshift-restclient-java:8.0.0.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.squareup.okhttp3:okhttp:3.14.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.squareup.okio:okio:1.17.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss:jboss-dmr:1.3.0.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.commons:commons-compress:1.18" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.slf4j:slf4j-log4j12:1.6.4" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: log4j:log4j:1.2.16" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: commons-lang:commons-lang:2.6" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.webauthn4j:webauthn4j-core:0.12.0.RELEASE" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.webauthn4j:webauthn4j-util:0.12.0.RELEASE" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.kerby:kerby-asn1:2.0.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.11.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-model-jpa:16.1.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.liquibase:liquibase-core:3.5.5" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.persistence:jakarta.persistence-api:2.2.3" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.hibernate:hibernate-core:5.3.20.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: javax.persistence:javax.persistence-api:2.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.javassist:javassist:3.23.2-GA" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: net.bytebuddy:byte-buddy:1.9.11" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: antlr:antlr:2.7.7" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.1.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss:jandex:2.0.5.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml:classmate:1.3.4" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.dom4j:dom4j:2.1.3" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.hibernate.common:hibernate-commons-annotations:5.0.4.Final" level="project" />
-    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
-    <orderEntry type="library" name="Maven: com.unboundid.product.scim2:scim2-sdk-client:2.3.7" level="project" />
-    <orderEntry type="library" name="Maven: com.unboundid.product.scim2:scim2-sdk-common:2.3.7" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: javax.ws.rs:javax.ws.rs-api:2.1.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: javax.xml.bind:jaxb-api:2.3.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: javax.activation:javax.activation-api:1.2.0" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-core:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.xml.bind:jakarta.xml.bind-api:2.3.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.activation:jakarta.activation-api:1.2.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.12.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jaxrs:3.15.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec:2.0.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.reactivestreams:reactive-streams:1.0.3" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.annotation:jboss-annotations-api_1.3_spec:2.0.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.activation:jakarta.activation:1.2.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpclient:4.5.13" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpcore:4.4.13" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: commons-logging:commons-logging:1.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: commons-io:commons-io:2.5" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.stephenc.jcip:jcip-annotations:1.0-1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-multipart-provider:3.15.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.james:apache-mime4j:0.6" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jackson2-provider:3.15.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:json-patch:1.9" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:jackson-coreutils:1.6" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:msg-simple:1.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:btf:1.2" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:28.1-jre" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:failureaccess:1.0.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.checkerframework:checker-qual:2.8.1" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jaxb-provider:3.15.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish.jaxb:jaxb-runtime:2.3.3-b02" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish.jaxb:txw2:2.3.3-b02" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.istack:istack-commons-runtime:3.0.10" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-client:3.15.1.Final" level="project" />
-    <orderEntry type="library" scope="PROVIDED" name="Maven: commons-codec:commons-codec:1.15" level="project" />
-    <orderEntry type="library" name="Maven: io.github.resilience4j:resilience4j-retry:1.7.0" level="project" />
-    <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" />
-    <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" />
-    <orderEntry type="library" name="Maven: io.github.resilience4j:resilience4j-core:1.7.0" level="project" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/src/main/java/sh/libre/scim/core/ScimDispatcher.java
deleted file mode 100644
index d3d675108bfd300398a80d9f97ecbf8115b15387..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/ScimDispatcher.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package sh.libre.scim.core;
-
-import org.jboss.logging.Logger;
-import org.keycloak.component.ComponentModel;
-import org.keycloak.models.KeycloakSession;
-import sh.libre.scim.core.exceptions.ScimExceptionHandler;
-import sh.libre.scim.core.exceptions.ScimPropagationException;
-import sh.libre.scim.core.exceptions.SkipOrStopApproach;
-import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
-import sh.libre.scim.core.service.AbstractScimService;
-import sh.libre.scim.core.service.GroupScimService;
-import sh.libre.scim.core.service.UserScimService;
-
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * In charge of sending SCIM Request to all registered Scim endpoints.
- */
-public class ScimDispatcher {
-
-    private static final Logger LOGGER = Logger.getLogger(ScimDispatcher.class);
-
-    private final KeycloakSession session;
-    private final ScimExceptionHandler exceptionHandler;
-    private final SkipOrStopStrategy skipOrStopStrategy;
-    private boolean clientsInitialized = false;
-    private final List<UserScimService> userScimServices = new ArrayList<>();
-    private final List<GroupScimService> groupScimServices = new ArrayList<>();
-
-
-    public ScimDispatcher(KeycloakSession session) {
-        this.session = session;
-        this.exceptionHandler = new ScimExceptionHandler(session);
-        // By default, use a permissive Skip or Stop strategy
-        this.skipOrStopStrategy = SkipOrStopApproach.ALWAYS_SKIP_AND_CONTINUE;
-    }
-
-    /**
-     * Lists all active ScimStorageProviderFactory and create new ScimClients for each of them
-     */
-    public void refreshActiveScimEndpoints() {
-        // Step 1: close existing clients (as configuration may have changed)
-        groupScimServices.forEach(GroupScimService::close);
-        groupScimServices.clear();
-        userScimServices.forEach(UserScimService::close);
-        userScimServices.clear();
-
-        // Step 2: Get All SCIM endpoints defined in Admin Console (enabled ScimStorageProviderFactory)
-        session.getContext().getRealm().getComponentsStream()
-                .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId())
-                             && m.get("enabled", true))
-                .forEach(scimEndpointConfigurationRaw -> {
-                    try {
-                        ScrimEndPointConfiguration scrimEndPointConfiguration = new ScrimEndPointConfiguration(scimEndpointConfigurationRaw);
-
-                        // Step 3 : create scim clients for each endpoint
-                        if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP, false)) {
-                            GroupScimService groupScimService = new GroupScimService(session, scrimEndPointConfiguration, skipOrStopStrategy);
-                            groupScimServices.add(groupScimService);
-                        }
-                        if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER, false)) {
-                            UserScimService userScimService = new UserScimService(session, scrimEndPointConfiguration, skipOrStopStrategy);
-                            userScimServices.add(userScimService);
-                        }
-                    } catch (IllegalArgumentException e) {
-                        if (skipOrStopStrategy.allowInvalidEndpointConfiguration()) {
-                            LOGGER.warn("[SCIM] Invalid Endpoint configuration " + scimEndpointConfigurationRaw.getId(), e);
-                        } else {
-                            throw e;
-                        }
-                    }
-                });
-    }
-
-    public void dispatchUserModificationToAll(SCIMPropagationConsumer<UserScimService> operationToDispatch) {
-        initializeClientsIfNeeded();
-        Set<UserScimService> servicesCorrectlyPropagated = new LinkedHashSet<>();
-        userScimServices.forEach(userScimService -> {
-            try {
-                operationToDispatch.acceptThrows(userScimService);
-                servicesCorrectlyPropagated.add(userScimService);
-            } catch (ScimPropagationException e) {
-                exceptionHandler.handleException(userScimService.getConfiguration(), e);
-            }
-        });
-        // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints
-        LOGGER.infof("[SCIM] User operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size());
-    }
-
-    public void dispatchGroupModificationToAll(SCIMPropagationConsumer<GroupScimService> operationToDispatch) {
-        initializeClientsIfNeeded();
-        Set<GroupScimService> servicesCorrectlyPropagated = new LinkedHashSet<>();
-        groupScimServices.forEach(groupScimService -> {
-            try {
-                operationToDispatch.acceptThrows(groupScimService);
-                servicesCorrectlyPropagated.add(groupScimService);
-            } catch (ScimPropagationException e) {
-                exceptionHandler.handleException(groupScimService.getConfiguration(), e);
-            }
-        });
-        // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints
-        LOGGER.infof("[SCIM] Group operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size());
-    }
-
-    public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<UserScimService> operationToDispatch) {
-        initializeClientsIfNeeded();
-        // Scim client should already have been created
-        Optional<UserScimService> matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst();
-        if (matchingClient.isPresent()) {
-            try {
-                operationToDispatch.acceptThrows(matchingClient.get());
-                LOGGER.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName());
-            } catch (ScimPropagationException e) {
-                exceptionHandler.handleException(matchingClient.get().getConfiguration(), e);
-            }
-        } else {
-            LOGGER.error("[SCIM] Could not find a Scim Client matching User endpoint configuration" + scimServerConfiguration.getId());
-        }
-    }
-
-
-    public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<GroupScimService> operationToDispatch) {
-        initializeClientsIfNeeded();
-        // Scim client should already have been created
-        Optional<GroupScimService> matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst();
-        if (matchingClient.isPresent()) {
-            try {
-                operationToDispatch.acceptThrows(matchingClient.get());
-                LOGGER.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName());
-            } catch (ScimPropagationException e) {
-                exceptionHandler.handleException(matchingClient.get().getConfiguration(), e);
-            }
-        } else {
-            LOGGER.error("[SCIM] Could not find a Scim Client matching Group endpoint configuration" + scimServerConfiguration.getId());
-        }
-    }
-
-    public void close() {
-        for (GroupScimService c : groupScimServices) {
-            c.close();
-        }
-        for (UserScimService c : userScimServices) {
-            c.close();
-        }
-        groupScimServices.clear();
-        userScimServices.clear();
-    }
-
-    private void initializeClientsIfNeeded() {
-        if (!clientsInitialized) {
-            clientsInitialized = true;
-            refreshActiveScimEndpoints();
-        }
-    }
-
-    /**
-     * A Consumer that throws ScimPropagationException.
-     *
-     * @param <T> An {@link AbstractScimService to call}
-     */
-    @FunctionalInterface
-    public interface SCIMPropagationConsumer<T> {
-
-        void acceptThrows(T elem) throws ScimPropagationException;
-
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java b/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java
deleted file mode 100644
index b73df47725de371ce9c3cf548bb16a86e0a4d493..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package sh.libre.scim.core;
-
-import de.captaingoldfish.scim.sdk.common.constants.HttpHeader;
-import jakarta.ws.rs.core.MediaType;
-import org.apache.commons.lang3.BooleanUtils;
-import org.jboss.logging.Logger;
-import org.keycloak.component.ComponentModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.provider.ProviderConfigurationBuilder;
-import org.keycloak.storage.UserStorageProvider;
-import org.keycloak.storage.UserStorageProviderFactory;
-import org.keycloak.storage.UserStorageProviderModel;
-import org.keycloak.storage.user.ImportSynchronization;
-import org.keycloak.storage.user.SynchronizationResult;
-import sh.libre.scim.event.ScimBackgroundGroupMembershipUpdater;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * Allows to register and configure Scim endpoints through Admin console, using the provided config properties.
- */
-public class ScimEndpointConfigurationStorageProviderFactory
-        implements UserStorageProviderFactory<ScimEndpointConfigurationStorageProviderFactory.ScimEndpointConfigurationStorageProvider>, ImportSynchronization {
-    public static final String ID = "scim";
-    private static final Logger LOGGER = Logger.getLogger(ScimEndpointConfigurationStorageProviderFactory.class);
-
-    @Override
-    public String getId() {
-        return ID;
-    }
-
-
-    @Override
-    public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId,
-                                      UserStorageProviderModel model) {
-        // Manually Launch a synchronization between keycloack and the SCIM endpoint described in the given model
-        LOGGER.infof("[SCIM] Sync from ScimStorageProvider - Realm %s - Model %s", realmId, model.getName());
-        SynchronizationResult result = new SynchronizationResult();
-        KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> {
-            RealmModel realm = session.realms().getRealm(realmId);
-            session.getContext().setRealm(realm);
-            ScimDispatcher dispatcher = new ScimDispatcher(session);
-            if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER))) {
-                dispatcher.dispatchUserModificationToOne(model, client -> client.sync(result));
-            }
-            if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP))) {
-                dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result));
-            }
-            dispatcher.close();
-        });
-        return result;
-    }
-
-    @Override
-    public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId,
-                                           UserStorageProviderModel model) {
-        return this.sync(sessionFactory, realmId, model);
-    }
-
-    @Override
-    public void postInit(KeycloakSessionFactory factory) {
-        ScimBackgroundGroupMembershipUpdater scimBackgroundGroupMembershipUpdater = new ScimBackgroundGroupMembershipUpdater(factory);
-        scimBackgroundGroupMembershipUpdater.startBackgroundUpdates();
-    }
-
-    @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        // These Config Properties will be use to generate configuration page in Admin Console
-        return ProviderConfigurationBuilder.create()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_ENDPOINT)
-                .type(ProviderConfigProperty.STRING_TYPE)
-                .required(true)
-                .label("SCIM 2.0 endpoint")
-                .helpText("External SCIM 2.0 base " +
-                          "URL (/ServiceProviderConfig  /Schemas and /ResourcesTypes should be accessible)")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_CONTENT_TYPE)
-                .type(ProviderConfigProperty.LIST_TYPE)
-                .label("Endpoint content type")
-                .helpText("Only used when endpoint doesn't support application/scim+json")
-                .options(MediaType.APPLICATION_JSON, HttpHeader.SCIM_CONTENT_TYPE)
-                .defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_MODE)
-                .type(ProviderConfigProperty.LIST_TYPE)
-                .label("Auth mode")
-                .helpText("Select the authorization mode")
-                .options("NONE", "BASIC_AUTH", "BEARER")
-                .defaultValue("NONE")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_USER)
-                .type(ProviderConfigProperty.STRING_TYPE)
-                .label("Auth username")
-                .helpText("Required for basic authentication.")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_PASSWORD)
-                .type(ProviderConfigProperty.PASSWORD)
-                .label("Auth password/token")
-                .helpText("Password or token required for basic or bearer authentication.")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER)
-                .type(ProviderConfigProperty.BOOLEAN_TYPE)
-                .label("Enable user propagation")
-                .helpText("Should operation on users be propagated to this provider?")
-                .defaultValue(BooleanUtils.TRUE)
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP)
-                .type(ProviderConfigProperty.BOOLEAN_TYPE)
-                .label("Enable group propagation")
-                .helpText("Should operation on groups be propagated to this provider?")
-                .defaultValue(BooleanUtils.TRUE)
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT)
-                .type(ProviderConfigProperty.BOOLEAN_TYPE)
-                .label("Enable import during sync")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT_ACTION)
-                .type(ProviderConfigProperty.LIST_TYPE)
-                .label("Import action")
-                .helpText("What to do when the user doesn't exists in Keycloak.")
-                .options("NOTHING", "CREATE_LOCAL", "DELETE_REMOTE")
-                .defaultValue("CREATE_LOCAL")
-                .add()
-                .property()
-                .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_REFRESH)
-                .type(ProviderConfigProperty.BOOLEAN_TYPE)
-                .label("Enable refresh during sync")
-                .name(ScrimEndPointConfiguration.CONF_KEY_LOG_ALL_SCIM_REQUESTS)
-                .type(ProviderConfigProperty.BOOLEAN_TYPE)
-                .label("Log SCIM requests and responses")
-                .helpText("If true, all sent SCIM requests and responses will be logged")
-                .add()
-                .build();
-    }
-
-
-    @Override
-    public ScimEndpointConfigurationStorageProvider create(KeycloakSession session, ComponentModel model) {
-        return new ScimEndpointConfigurationStorageProvider();
-    }
-
-    /**
-     * Empty implementation : we used this {@link ScimEndpointConfigurationStorageProviderFactory} to generate Admin Console page.
-     */
-    public static final class ScimEndpointConfigurationStorageProvider implements UserStorageProvider {
-        @Override
-        public void close() {
-            // Nothing to close here
-        }
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java b/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java
deleted file mode 100644
index 6359b571535cc9dceafc5ce7f1d4a7ecf1e0ff01..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package sh.libre.scim.core;
-
-import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
-import org.keycloak.component.ComponentModel;
-
-public class ScrimEndPointConfiguration {
-    // Configuration keys : also used in Admin Console page
-    public static final String CONF_KEY_AUTH_MODE = "auth-mode";
-    public static final String CONF_KEY_AUTH_PASSWORD = "auth-pass";
-    public static final String CONF_KEY_AUTH_USER = "auth-user";
-    public static final String CONF_KEY_CONTENT_TYPE = "content-type";
-    public static final String CONF_KEY_ENDPOINT = "endpoint";
-    public static final String CONF_KEY_SYNC_IMPORT_ACTION = "sync-import-action";
-    public static final String CONF_KEY_SYNC_IMPORT = "sync-import";
-    public static final String CONF_KEY_SYNC_REFRESH = "sync-refresh";
-    public static final String CONF_KEY_PROPAGATION_USER = "propagation-user";
-    public static final String CONF_KEY_PROPAGATION_GROUP = "propagation-group";
-    public static final String CONF_KEY_LOG_ALL_SCIM_REQUESTS = "log-all-scim-requests";
-
-    private final String endPoint;
-    private final String id;
-    private final String name;
-    private final String contentType;
-    private final String authorizationHeaderValue;
-    private final ImportAction importAction;
-    private final boolean pullFromScimSynchronisationActivated;
-    private final boolean pushToScimSynchronisationActivated;
-    private final boolean logAllScimRequests;
-
-    public ScrimEndPointConfiguration(ComponentModel scimProviderConfiguration) {
-        try {
-            AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get(CONF_KEY_AUTH_MODE));
-
-            authorizationHeaderValue = switch (authMode) {
-                case BEARER -> "Bearer " + scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD);
-                case BASIC_AUTH -> {
-                    BasicAuth basicAuth = BasicAuth.builder()
-                            .username(scimProviderConfiguration.get(CONF_KEY_AUTH_USER))
-                            .password(scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD))
-                            .build();
-                    yield basicAuth.getAuthorizationHeaderValue();
-                }
-                case NONE -> "";
-            };
-            contentType = scimProviderConfiguration.get(CONF_KEY_CONTENT_TYPE, "");
-            endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT, "");
-            id = scimProviderConfiguration.getId();
-            name = scimProviderConfiguration.getName();
-            importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION));
-            pullFromScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false);
-            pushToScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false);
-            logAllScimRequests = scimProviderConfiguration.get(CONF_KEY_LOG_ALL_SCIM_REQUESTS, false);
-        } catch (IllegalArgumentException e) {
-            throw new IllegalArgumentException("authMode '" + scimProviderConfiguration.get(CONF_KEY_AUTH_MODE) + "' is not supported");
-        }
-    }
-
-    public boolean isPushToScimSynchronisationActivated() {
-        return pushToScimSynchronisationActivated;
-    }
-
-    public boolean isPullFromScimSynchronisationActivated() {
-        return pullFromScimSynchronisationActivated;
-    }
-
-    public String getContentType() {
-        return contentType;
-    }
-
-    public String getAuthorizationHeaderValue() {
-        return authorizationHeaderValue;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public ImportAction getImportAction() {
-        return importAction;
-    }
-
-    public String getEndPoint() {
-        return endPoint;
-    }
-
-    public boolean isLogAllScimRequests() {
-        return logAllScimRequests;
-    }
-
-    public enum AuthMode {
-        BEARER, BASIC_AUTH, NONE
-    }
-
-    public enum ImportAction {
-        CREATE_LOCAL, DELETE_REMOTE, NOTHING
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java b/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java
deleted file mode 100644
index 44f7eb46e67fb86268db51c376cee94936e0aaac..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-public class InconsistentScimMappingException extends ScimPropagationException {
-    public InconsistentScimMappingException(String message) {
-        super(message);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java b/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java
deleted file mode 100644
index 079443622be9e2a9f77031e2b1a8c00b6ca557d3..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
-
-import java.util.Optional;
-
-public class InvalidResponseFromScimEndpointException extends ScimPropagationException {
-
-    private final transient Optional<ServerResponse> response;
-
-    public InvalidResponseFromScimEndpointException(ServerResponse response, String message) {
-        super(message);
-        this.response = Optional.of(response);
-    }
-
-    public InvalidResponseFromScimEndpointException(String message, Exception e) {
-        super(message, e);
-        this.response = Optional.empty();
-    }
-
-
-    /**
-     * Empty response can occur if a major exception was thrown while retrying the request.
-     */
-    public Optional<ServerResponse> getResponse() {
-        return response;
-    }
-
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java b/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java
deleted file mode 100644
index d1fb108930679b91dc213e8e3d77b7c68b5313d0..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import com.google.common.collect.Lists;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-
-import java.util.ArrayList;
-
-
-public enum RollbackApproach implements RollbackStrategy {
-    ALWAYS_ROLLBACK {
-        @Override
-        public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
-            return true;
-        }
-    },
-    NEVER_ROLLBACK {
-        @Override
-        public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
-            return false;
-        }
-    },
-    CRITICAL_ONLY_ROLLBACK {
-        @Override
-        public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
-            if (e instanceof InconsistentScimMappingException) {
-                // Occurs when mapping between a SCIM resource and a keycloak user failed (missing, ambiguous..)
-                // Log can be sufficient here, no rollback required
-                return false;
-            }
-            if (e instanceof UnexpectedScimDataException) {
-                // Occurs when a SCIM endpoint sends invalid date (e.g. group with empty name, user without ids...)
-                // No rollback required : we cannot recover. This needs to be fixed in the SCIM endpoint data
-                return false;
-            }
-            if (e instanceof InvalidResponseFromScimEndpointException invalidResponseFromScimEndpointException) {
-                return shouldRollbackBecauseOfResponse(invalidResponseFromScimEndpointException);
-            }
-            // Should not occur
-            throw new IllegalStateException("Unkown ScimPropagationException", e);
-        }
-
-        private boolean shouldRollbackBecauseOfResponse(InvalidResponseFromScimEndpointException e) {
-            // If we have a response
-            return e.getResponse().map(r -> {
-                // We consider that 404 are acceptable, otherwise rollback
-                ArrayList<Integer> acceptableStatus = Lists.newArrayList(200, 204, 404);
-                return !acceptableStatus.contains(r.getHttpStatus());
-            }).orElse(
-                    // Never got an answer, server was either misconfigured or unreachable
-                    // No rollback in that case.
-                    false
-            );
-        }
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java b/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java
deleted file mode 100644
index 90d859305c22e63641edf1acc2e3d4421250fa0d..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-
-/**
- * In charge of deciding, when facing a SCIM-related issue during an operation (e.g User creation),
- * whether we should :
- * - Log the issue and let the operation succeed in Keycloack database (potentially unsynchronising
- * Keycloack with the SCIM servers)
- * - Rollback the whole operation
- */
-public interface RollbackStrategy {
-
-    /**
-     * Indicates whether we should rollback the whole transaction because of the given exception.
-     *
-     * @param configuration The SCIM Endpoint configuration for which the exception occured
-     * @param e             the exception that we have to handle
-     * @return true if transaction should be rolled back, false if we should log and continue operation
-     */
-    boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e);
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java b/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java
deleted file mode 100644
index 78973d2ad6e4ad9ee2fb32d73544d8786f2b6f39..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import org.jboss.logging.Logger;
-import org.keycloak.models.KeycloakSession;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-
-/**
- * In charge of dealing with SCIM exceptions by ignoring, logging or rollback transaction according to :
- * - The context in which it occurs (sync, user creation...)
- * - The related SCIM endpoint and its configuration
- * - The thrown exception itself
- */
-public class ScimExceptionHandler {
-    private static final Logger LOGGER = Logger.getLogger(ScimExceptionHandler.class);
-
-    private final KeycloakSession session;
-    private final RollbackStrategy rollbackStrategy;
-
-    public ScimExceptionHandler(KeycloakSession session) {
-        this(session, RollbackApproach.CRITICAL_ONLY_ROLLBACK);
-    }
-
-    public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) {
-        this.session = session;
-        this.rollbackStrategy = rollbackStrategy;
-    }
-
-    /**
-     * Handles the given exception by loggin and/or rollback transaction.
-     *
-     * @param scimProviderConfiguration the configuration of the endpoint for which the propagation exception occured
-     * @param e                         the occuring exception
-     */
-    public void handleException(ScrimEndPointConfiguration scimProviderConfiguration, ScimPropagationException e) {
-        String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getName();
-        if (rollbackStrategy.shouldRollback(scimProviderConfiguration, e)) {
-            session.getTransactionManager().rollback();
-            LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e);
-        } else {
-            LOGGER.warn(errorMessage, e);
-        }
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java b/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java
deleted file mode 100644
index bee5ee18fdfb0babae5203f0559c6427cbff9bac..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-public abstract class ScimPropagationException extends Exception {
-
-    protected ScimPropagationException(String message) {
-        super(message);
-    }
-
-    protected ScimPropagationException(String message, Exception e) {
-        super(message, e);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java b/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java
deleted file mode 100644
index e0669d59db69f3d8f4b43a435c1fbbc72b08f379..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-
-
-public enum SkipOrStopApproach implements SkipOrStopStrategy {
-    ALWAYS_SKIP_AND_CONTINUE {
-        @Override
-        public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) {
-            return false;
-        }
-
-        @Override
-        public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) {
-            return false;
-        }
-
-        @Override
-        public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) {
-            return false;
-        }
-
-        @Override
-        public boolean allowInvalidEndpointConfiguration() {
-            return false;
-        }
-
-        @Override
-        public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) {
-            return false;
-        }
-    },
-    ALWAYS_STOP {
-        @Override
-        public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) {
-            return true;
-        }
-
-        @Override
-        public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) {
-            return true;
-        }
-
-        @Override
-        public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) {
-            return true;
-        }
-
-        @Override
-        public boolean allowInvalidEndpointConfiguration() {
-            return true;
-        }
-
-        @Override
-        public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) {
-            return true;
-        }
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java b/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java
deleted file mode 100644
index 8ad46c7ff970b86a17a7b9bd39ed8030b9f459d1..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-
-/**
- * In charge of deciding, when facing a SCIM-related issue, whether we should :
- * - log a warning, skip the problematic element and continue the rest of the operation
- * - stop immediately the whole operation (typically, a synchronisation between SCIM and Keycloack)
- */
-public interface SkipOrStopStrategy {
-    /**
-     * Indicates if,  during a synchronisation from Keycloack to a SCIM endpoint, we should :
-     * - cancel the whole synchronisation if an element CRUD fail, or
-     * - keep on with synchronisation, allowing a partial synchronisation
-     *
-     * @param configuration the configuration of the endpoint in which the error occurred
-     * @return true if a partial synchronisation is allowed,
-     * false if we should stop the whole synchronisation at first issue
-     */
-    boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration);
-
-    /**
-     * Indicates if,  during a synchronisation from a SCIM endpoint to Keycloack, we should :
-     * - cancel the whole synchronisation if an element CRUD fail, or
-     * - keep on with synchronisation, allowing a partial synchronisation
-     *
-     * @param configuration the configuration of the endpoint in which the error occurred
-     * @return true if a partial synchronisation is allowed,
-     * false if we should interrupt the whole synchronisation at first issue
-     */
-    boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration);
-
-
-    /**
-     * Indicates if, when we propagate a group creation or update to a SCIM endpoint and some
-     * of its members are not mapped to SCIM, we should allow partial group update or interrupt completely.
-     *
-     * @param configuration the configuration of the endpoint in which the error occurred
-     * @return true if a partial group update is allowed,
-     * false if we should interrupt the group update in case of any unmapped member
-     */
-    boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration);
-
-    /**
-     * Indicates if, when facing an invalid SCIM endpoint configuration (resulting in a unreachable SCIM server),
-     * we should stop or ignore this configuration.
-     *
-     * @return true the invalid endpoint should be ignored,
-     * * false if we should interrupt the rest of the synchronisation
-     */
-    boolean allowInvalidEndpointConfiguration();
-
-    /**
-     * Indicates if, when trying to pull User or Groups from a SCIM endpoint,
-     * we encounter a invalid data (e.g. group with empty name), we should :
-     * - Skip the invalid element pull and continue
-     * - Cancel the whole synchronisation
-     *
-     * @param configuration the configuration of the endpoint in which the error occurred
-     * @return true if we should skip the invalid data synchronisation and pursue,
-     * false if we should interrupt immediately the whole synchronisation
-     */
-    boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration);
-
-
-}
diff --git a/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java b/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java
deleted file mode 100644
index 918127ef0b3c2244d26ddeb41a16a1e44c40befb..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package sh.libre.scim.core.exceptions;
-
-public class UnexpectedScimDataException extends ScimPropagationException {
-    public UnexpectedScimDataException(String message) {
-        super(message);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java b/src/main/java/sh/libre/scim/core/service/AbstractScimService.java
deleted file mode 100644
index b22b6a9a8d60ee58634940418ce437b4446f3c35..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java
+++ /dev/null
@@ -1,281 +0,0 @@
-package sh.libre.scim.core.service;
-
-import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
-import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
-import org.jboss.logging.Logger;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RoleMapperModel;
-import org.keycloak.storage.user.SynchronizationResult;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
-import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException;
-import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
-import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
-import sh.libre.scim.jpa.ScimResourceDao;
-import sh.libre.scim.jpa.ScimResourceMapping;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * A service in charge of synchronisation (CRUD) between
- * a Keykloak Role (UserModel, GroupModel) and a SCIM Resource (User,Group).
- *
- * @param <K> The Keycloack Model (e.g. UserModel, GroupModel)
- * @param <S> The SCIM Resource (e.g. User, Group)
- */
-public abstract class AbstractScimService<K extends RoleMapperModel, S extends ResourceNode> implements AutoCloseable {
-
-    private static final Logger LOGGER = Logger.getLogger(AbstractScimService.class);
-
-    private final KeycloakSession keycloakSession;
-    protected final SkipOrStopStrategy skipOrStopStrategy;
-    private final ScrimEndPointConfiguration scimProviderConfiguration;
-    private final ScimResourceType type;
-    private final ScimClient<S> scimClient;
-
-    protected AbstractScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType type, SkipOrStopStrategy skipOrStopStrategy) {
-        this.keycloakSession = keycloakSession;
-        this.scimProviderConfiguration = scimProviderConfiguration;
-        this.type = type;
-        this.scimClient = ScimClient.open(scimProviderConfiguration, type);
-        this.skipOrStopStrategy = skipOrStopStrategy;
-    }
-
-    public void create(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        if (isMarkedToIgnore(roleMapperModel)) {
-            // Silently return: resource is explicitly marked as to ignore
-            return;
-        }
-        // If mapping, then we are trying to recreate a user that was already created by import
-        KeycloakId id = getId(roleMapperModel);
-        if (findMappingById(id).isPresent()) {
-            throw new InconsistentScimMappingException("Trying to create user with id " + id + ": id already exists in Keycloak database");
-        }
-        S scimForCreation = scimRequestBodyForCreate(roleMapperModel);
-        EntityOnRemoteScimId externalId = scimClient.create(id, scimForCreation);
-        createMapping(id, externalId);
-    }
-
-    public void update(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        if (isMarkedToIgnore(roleMapperModel)) {
-            // Silently return: resource is explicitly marked as to ignore
-            return;
-        }
-        KeycloakId keycloakId = getId(roleMapperModel);
-        EntityOnRemoteScimId entityOnRemoteScimId = findMappingById(keycloakId)
-                .map(ScimResourceMapping::getExternalIdAsEntityOnRemoteScimId)
-                .orElseThrow(() -> new InconsistentScimMappingException("Failed to find SCIM mapping for " + keycloakId));
-        S scimForReplace = scimRequestBodyForUpdate(roleMapperModel, entityOnRemoteScimId);
-        scimClient.update(entityOnRemoteScimId, scimForReplace);
-    }
-
-    protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException;
-
-    public void delete(KeycloakId id) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        ScimResourceMapping resource = findMappingById(id)
-                .orElseThrow(() -> new InconsistentScimMappingException("Failed to delete resource %s, scim mapping not found: ".formatted(id)));
-        EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId();
-        scimClient.delete(externalId);
-        getScimResourceDao().delete(resource);
-    }
-
-    public void pushAllResourcesToScim(SynchronizationResult syncRes) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException {
-        LOGGER.info("[SCIM] Push resources to endpoint  " + this.getConfiguration().getEndPoint());
-        try (Stream<K> resourcesStream = getResourceStream()) {
-            Set<K> resources = resourcesStream.collect(Collectors.toUnmodifiableSet());
-            for (K resource : resources) {
-                KeycloakId id = getId(resource);
-                pushSingleResourceToScim(syncRes, resource, id);
-            }
-        }
-    }
-
-    public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint());
-        for (S resource : scimClient.listResources()) {
-            pullSingleResourceFromScim(syncRes, resource);
-        }
-    }
-
-    private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException {
-        try {
-            LOGGER.infof("[SCIM] Reconciling local resource %s", id);
-            if (shouldIgnoreForScimSynchronization(resource)) {
-                LOGGER.infof("[SCIM] Skip local resource %s", id);
-                return;
-            }
-            if (findMappingById(id).isPresent()) {
-                LOGGER.info("[SCIM] Replacing it");
-                update(resource);
-            } else {
-                LOGGER.info("[SCIM] Creating it");
-                create(resource);
-            }
-            syncRes.increaseUpdated();
-        } catch (InvalidResponseFromScimEndpointException e) {
-            if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
-                LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint(), e);
-            } else {
-                throw e;
-            }
-        } catch (InconsistentScimMappingException e) {
-            if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
-                LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint(), e);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-
-    private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        try {
-            LOGGER.infof("[SCIM] Reconciling remote resource %s", resource);
-            EntityOnRemoteScimId externalId = resource.getId()
-                    .map(EntityOnRemoteScimId::new)
-                    .orElseThrow(() -> new UnexpectedScimDataException("Remote SCIM resource doesn't have an id, cannot import it in Keycloak"));
-            if (validMappingAlreadyExists(externalId)) return;
-
-            // Here no keycloak user/group matching the SCIM external id exists
-            // Try to match existing keycloak resource by properties (username, email, name)
-            Optional<KeycloakId> mapped = matchKeycloakMappingByScimProperties(resource);
-            if (mapped.isPresent()) {
-                // If found a mapped, update
-                LOGGER.info("[SCIM] Matched SCIM resource " + externalId + " from properties with keycloak entity " + mapped.get());
-                createMapping(mapped.get(), externalId);
-                syncRes.increaseUpdated();
-            } else {
-                // If not, create it locally or deleting it remotely (according to the configured Import Action)
-                createLocalOrDeleteRemote(syncRes, resource, externalId);
-            }
-        } catch (UnexpectedScimDataException e) {
-            if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) {
-                LOGGER.warn("[SCIM] Skipping element synchronisation because of invalid Scim Data for element " + resource.getId() + " : " + e.getMessage(), e);
-            } else {
-                throw e;
-            }
-        } catch (InconsistentScimMappingException e) {
-            if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
-                LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage(), e);
-            } else {
-                throw e;
-            }
-        } catch (InvalidResponseFromScimEndpointException e) {
-            // Can only occur in case of a DELETE_REMOTE conflict action
-            if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
-                LOGGER.warn("[SCIM] Could not  delete SCIM resource " + resource.getId() + " during synchronisation", e);
-            } else {
-                throw e;
-            }
-        }
-
-    }
-
-    private boolean validMappingAlreadyExists(EntityOnRemoteScimId externalId) {
-        Optional<ScimResourceMapping> optionalMapping = getScimResourceDao().findByExternalId(externalId, type);
-        // If an existing mapping exists, delete potential dangling references
-        if (optionalMapping.isPresent()) {
-            ScimResourceMapping mapping = optionalMapping.get();
-            if (entityExists(mapping.getIdAsKeycloakId())) {
-                LOGGER.info("[SCIM] Valid mapping found, skipping");
-                return true;
-            } else {
-                LOGGER.info("[SCIM] Delete a dangling mapping");
-                getScimResourceDao().delete(mapping);
-            }
-        }
-        return false;
-    }
-
-    private void createLocalOrDeleteRemote(SynchronizationResult syncRes, S resource, EntityOnRemoteScimId externalId) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
-        switch (scimProviderConfiguration.getImportAction()) {
-            case CREATE_LOCAL -> {
-                LOGGER.info("[SCIM] Create local resource for SCIM resource " + externalId);
-                KeycloakId id = createEntity(resource);
-                createMapping(id, externalId);
-                syncRes.increaseAdded();
-            }
-            case DELETE_REMOTE -> {
-                LOGGER.info("[SCIM] Delete remote resource " + externalId);
-                scimClient.delete(externalId);
-            }
-            case NOTHING -> LOGGER.info("[SCIM] Import action set to NOTHING");
-        }
-    }
-
-
-    protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimMappingException;
-
-    protected abstract KeycloakId getId(K roleMapperModel);
-
-    protected abstract boolean isMarkedToIgnore(K roleMapperModel);
-
-    private void createMapping(KeycloakId keycloakId, EntityOnRemoteScimId externalId) {
-        getScimResourceDao().create(keycloakId, externalId, type);
-    }
-
-    protected ScimResourceDao getScimResourceDao() {
-        return ScimResourceDao.newInstance(getKeycloakSession(), scimProviderConfiguration.getId());
-    }
-
-    private Optional<ScimResourceMapping> findMappingById(KeycloakId keycloakId) {
-        return getScimResourceDao().findById(keycloakId, type);
-    }
-
-    private KeycloakSession getKeycloakSession() {
-        return keycloakSession;
-    }
-
-
-    protected abstract boolean shouldIgnoreForScimSynchronization(K resource);
-
-    protected abstract Stream<K> getResourceStream();
-
-    protected abstract KeycloakId createEntity(S resource) throws UnexpectedScimDataException, InconsistentScimMappingException;
-
-    protected abstract Optional<KeycloakId> matchKeycloakMappingByScimProperties(S resource) throws InconsistentScimMappingException;
-
-    protected abstract boolean entityExists(KeycloakId keycloakId);
-
-    public void sync(SynchronizationResult syncRes) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException, UnexpectedScimDataException {
-        if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) {
-            this.pullAllResourcesFromScim(syncRes);
-        }
-        if (this.scimProviderConfiguration.isPushToScimSynchronisationActivated()) {
-            this.pushAllResourcesToScim(syncRes);
-        }
-    }
-
-    protected Meta newMetaLocation(EntityOnRemoteScimId externalId) {
-        Meta meta = new Meta();
-        URI uri = getUri(type, externalId);
-        meta.setLocation(uri.toString());
-        return meta;
-    }
-
-    protected URI getUri(ScimResourceType type, EntityOnRemoteScimId externalId) {
-        try {
-            return new URI("%s/%s".formatted(type.getEndpoint(), externalId.asString()));
-        } catch (URISyntaxException e) {
-            throw new IllegalStateException("should never occur: can not format URI for type %s and id %s".formatted(type, externalId), e);
-        }
-    }
-
-    protected KeycloakDao getKeycloakDao() {
-        return new KeycloakDao(getKeycloakSession());
-    }
-
-    @Override
-    public void close() {
-        scimClient.close();
-    }
-
-    public ScrimEndPointConfiguration getConfiguration() {
-        return scimProviderConfiguration;
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java b/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java
deleted file mode 100644
index df96a12323c933b683631b252b1b6529e94c5c5a..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package sh.libre.scim.core.service;
-
-public record EntityOnRemoteScimId(
-        String asString
-) {
-}
diff --git a/src/main/java/sh/libre/scim/core/service/GroupScimService.java b/src/main/java/sh/libre/scim/core/service/GroupScimService.java
deleted file mode 100644
index bd09f3e9b2442bad39b340eb59070eec278a244e..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/GroupScimService.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package sh.libre.scim.core.service;
-
-import de.captaingoldfish.scim.sdk.common.resources.Group;
-import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
-import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.BooleanUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.jboss.logging.Logger;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.UserModel;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
-import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
-import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
-import sh.libre.scim.jpa.ScimResourceMapping;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Stream;
-
-public class GroupScimService extends AbstractScimService<GroupModel, Group> {
-    private static final Logger LOGGER = Logger.getLogger(GroupScimService.class);
-
-    public GroupScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, SkipOrStopStrategy skipOrStopStrategy) {
-        super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP, skipOrStopStrategy);
-    }
-
-    @Override
-    protected Stream<GroupModel> getResourceStream() {
-        return getKeycloakDao().getGroupsStream();
-    }
-
-    @Override
-    protected boolean entityExists(KeycloakId keycloakId) {
-        return getKeycloakDao().groupExists(keycloakId);
-    }
-
-    @Override
-    protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(Group resource) {
-        Set<String> names = new TreeSet<>();
-        resource.getId().ifPresent(names::add);
-        resource.getDisplayName().ifPresent(names::add);
-        try (Stream<GroupModel> groupsStream = getKeycloakDao().getGroupsStream()) {
-            Optional<GroupModel> group = groupsStream
-                    .filter(groupModel -> names.contains(groupModel.getName()))
-                    .findFirst();
-            return group
-                    .map(GroupModel::getId)
-                    .map(KeycloakId::new);
-        }
-    }
-
-    @Override
-    protected KeycloakId createEntity(Group resource) throws UnexpectedScimDataException, InconsistentScimMappingException {
-        String displayName = resource.getDisplayName()
-                .filter(StringUtils::isNotBlank)
-                .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim group has empty name, can't create. Resource id = %s".formatted(resource.getId())));
-        GroupModel group = getKeycloakDao().createGroup(displayName);
-        List<Member> groupMembers = resource.getMembers();
-        if (CollectionUtils.isNotEmpty(groupMembers)) {
-            for (Member groupMember : groupMembers) {
-                EntityOnRemoteScimId externalId = groupMember.getValue()
-                        .map(EntityOnRemoteScimId::new)
-                        .orElseThrow(() -> new UnexpectedScimDataException("can't create group member for group '%s' without id: ".formatted(displayName) + resource));
-                KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId)
-                        .map(ScimResourceMapping::getIdAsKeycloakId)
-                        .orElseThrow(() -> new InconsistentScimMappingException("can't find mapping for group member %s".formatted(externalId)));
-                UserModel userModel = getKeycloakDao().getUserById(userId);
-                userModel.joinGroup(group);
-            }
-        }
-        return new KeycloakId(group.getId());
-    }
-
-    @Override
-    protected boolean isMarkedToIgnore(GroupModel groupModel) {
-        return BooleanUtils.TRUE.equals(groupModel.getFirstAttribute("scim-skip"));
-    }
-
-    @Override
-    protected KeycloakId getId(GroupModel groupModel) {
-        return new KeycloakId(groupModel.getId());
-    }
-
-    @Override
-    protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimMappingException {
-        Set<KeycloakId> members = getKeycloakDao().getGroupMembers(groupModel);
-        Group group = new Group();
-        group.setExternalId(groupModel.getId());
-        group.setDisplayName(groupModel.getName());
-        for (KeycloakId member : members) {
-            Member groupMember = new Member();
-            Optional<ScimResourceMapping> optionalGroupMemberMapping = getScimResourceDao().findUserById(member);
-            if (optionalGroupMemberMapping.isPresent()) {
-                ScimResourceMapping groupMemberMapping = optionalGroupMemberMapping.get();
-                EntityOnRemoteScimId externalIdAsEntityOnRemoteScimId = groupMemberMapping.getExternalIdAsEntityOnRemoteScimId();
-                groupMember.setValue(externalIdAsEntityOnRemoteScimId.asString());
-                URI ref = getUri(ScimResourceType.USER, externalIdAsEntityOnRemoteScimId);
-                groupMember.setRef(ref.toString());
-                group.addMember(groupMember);
-            } else {
-                String message = "Unmapped member " + member + " for group " + groupModel.getId();
-                if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) {
-                    LOGGER.warn(message);
-                } else {
-                    throw new InconsistentScimMappingException(message);
-                }
-            }
-        }
-        return group;
-    }
-
-    @Override
-    protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException {
-        Group group = scimRequestBodyForCreate(groupModel);
-        group.setId(externalId.asString());
-        Meta meta = newMetaLocation(externalId);
-        group.setMeta(meta);
-        return group;
-    }
-
-    @Override
-    protected boolean shouldIgnoreForScimSynchronization(GroupModel resource) {
-        return false;
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/service/KeycloakDao.java b/src/main/java/sh/libre/scim/core/service/KeycloakDao.java
deleted file mode 100644
index f4c406c351300660e86ef3e423515cc018924425..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/KeycloakDao.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package sh.libre.scim.core.service;
-
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-
-import java.util.Collections;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class KeycloakDao {
-
-    private final KeycloakSession keycloakSession;
-
-    public KeycloakDao(KeycloakSession keycloakSession) {
-        this.keycloakSession = keycloakSession;
-    }
-
-    private KeycloakSession getKeycloakSession() {
-        return keycloakSession;
-    }
-
-    private RealmModel getRealm() {
-        return getKeycloakSession().getContext().getRealm();
-    }
-
-    public boolean groupExists(KeycloakId groupId) {
-        GroupModel group = getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString());
-        return group != null;
-    }
-
-    public boolean userExists(KeycloakId userId) {
-        UserModel user = getUserById(userId);
-        return user != null;
-    }
-
-    public UserModel getUserById(KeycloakId userId) {
-        return getKeycloakSession().users().getUserById(getRealm(), userId.asString());
-    }
-
-    public GroupModel getGroupById(KeycloakId groupId) {
-        return getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString());
-    }
-
-
-    public Stream<GroupModel> getGroupsStream() {
-        return getKeycloakSession().groups().getGroupsStream(getRealm());
-    }
-
-    public GroupModel createGroup(String displayName) {
-        return getKeycloakSession().groups().createGroup(getRealm(), displayName);
-    }
-
-    public Set<KeycloakId> getGroupMembers(GroupModel groupModel) {
-        return getKeycloakSession().users()
-                .getGroupMembersStream(getRealm(), groupModel)
-                .map(UserModel::getId)
-                .map(KeycloakId::new)
-                .collect(Collectors.toSet());
-    }
-
-    public Stream<UserModel> getUsersStream() {
-        return getKeycloakSession().users().searchForUserStream(getRealm(), Collections.emptyMap());
-    }
-
-    public UserModel getUserByUsername(String username) {
-        return getKeycloakSession().users().getUserByUsername(getRealm(), username);
-    }
-
-    public UserModel getUserByEmail(String email) {
-        return getKeycloakSession().users().getUserByEmail(getRealm(), email);
-    }
-
-    public UserModel addUser(String username) {
-        return getKeycloakSession().users().addUser(getRealm(), username);
-    }
-
-
-}
diff --git a/src/main/java/sh/libre/scim/core/service/KeycloakId.java b/src/main/java/sh/libre/scim/core/service/KeycloakId.java
deleted file mode 100644
index 04bad470b3b40f6178ffde70b15a6e3f253f1d53..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/KeycloakId.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package sh.libre.scim.core.service;
-
-public record KeycloakId(
-        String asString
-) {
-
-}
diff --git a/src/main/java/sh/libre/scim/core/service/ScimClient.java b/src/main/java/sh/libre/scim/core/service/ScimClient.java
deleted file mode 100644
index de3b4d7c0f4b8d24d406249c8268c1d3f9d3d12c..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/ScimClient.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package sh.libre.scim.core.service;
-
-import com.google.common.net.HttpHeaders;
-import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
-import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
-import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
-import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
-import de.captaingoldfish.scim.sdk.common.response.ListResponse;
-import io.github.resilience4j.core.IntervalFunction;
-import io.github.resilience4j.retry.Retry;
-import io.github.resilience4j.retry.RetryConfig;
-import io.github.resilience4j.retry.RetryRegistry;
-import jakarta.ws.rs.ProcessingException;
-import org.jboss.logging.Logger;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-public class ScimClient<S extends ResourceNode> implements AutoCloseable {
-    private static final Logger LOGGER = Logger.getLogger(ScimClient.class);
-
-    private final RetryRegistry retryRegistry;
-
-    private final ScimRequestBuilder scimRequestBuilder;
-
-    private final ScimResourceType scimResourceType;
-    private final boolean logAllRequests;
-
-    private ScimClient(ScimRequestBuilder scimRequestBuilder, ScimResourceType scimResourceType, boolean detailedLogs) {
-        this.scimRequestBuilder = scimRequestBuilder;
-        this.scimResourceType = scimResourceType;
-        RetryConfig retryConfig = RetryConfig.custom()
-                .maxAttempts(10)
-                .intervalFunction(IntervalFunction.ofExponentialBackoff())
-                .retryExceptions(ProcessingException.class)
-                .build();
-        retryRegistry = RetryRegistry.of(retryConfig);
-        this.logAllRequests = detailedLogs;
-    }
-
-    public static <T extends ResourceNode> ScimClient<T> open(ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) {
-        String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint();
-        Map<String, String> httpHeaders = new HashMap<>();
-        httpHeaders.put(HttpHeaders.AUTHORIZATION, scimProviderConfiguration.getAuthorizationHeaderValue());
-        httpHeaders.put(HttpHeaders.CONTENT_TYPE, scimProviderConfiguration.getContentType());
-        ScimClientConfig scimClientConfig = ScimClientConfig.builder()
-                .httpHeaders(httpHeaders)
-                .connectTimeout(5)
-                .requestTimeout(5)
-                .socketTimeout(5)
-                .build();
-        ScimRequestBuilder scimRequestBuilder =
-                new ScimRequestBuilder(
-                        scimApplicationBaseUrl,
-                        scimClientConfig
-                );
-        return new ScimClient<>(scimRequestBuilder, scimResourceType, scimProviderConfiguration.isLogAllScimRequests());
-    }
-
-    public EntityOnRemoteScimId create(KeycloakId id, S scimForCreation) throws InvalidResponseFromScimEndpointException {
-        Optional<String> scimForCreationId = scimForCreation.getId();
-        if (scimForCreationId.isPresent()) {
-            throw new IllegalArgumentException(
-                    "User to create should never have an existing id: %s %s".formatted(id, scimForCreationId.get())
-            );
-        }
-        try {
-            Retry retry = retryRegistry.retry("create-%s".formatted(id.asString()));
-            if (logAllRequests) {
-                LOGGER.info("[SCIM] Sending CREATE " + scimForCreation.toPrettyString() + "\n to " + getScimEndpoint());
-            }
-            ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
-                    .create(getResourceClass(), getScimEndpoint())
-                    .setResource(scimForCreation)
-                    .sendRequest()
-            );
-            checkResponseIsSuccess(response);
-            S resource = response.getResource();
-            return resource.getId()
-                    .map(EntityOnRemoteScimId::new)
-                    .orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id"));
-
-        } catch (Exception e) {
-            LOGGER.warn(e);
-            throw new InvalidResponseFromScimEndpointException("Exception while retrying create " + e.getMessage(), e);
-        }
-    }
-
-    private void checkResponseIsSuccess(ServerResponse<S> response) throws InvalidResponseFromScimEndpointException {
-        if (logAllRequests) {
-            LOGGER.info("[SCIM] Server response " + response.getHttpStatus() + "\n" + response.getResponseBody());
-        }
-        if (!response.isSuccess()) {
-            throw new InvalidResponseFromScimEndpointException(response, "Server answered with status " + response.getResponseBody() + ": " + response.getResponseBody());
-        }
-    }
-
-    private String getScimEndpoint() {
-        return scimResourceType.getEndpoint();
-    }
-
-    private Class<S> getResourceClass() {
-        return scimResourceType.getResourceClass();
-    }
-
-    public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws InvalidResponseFromScimEndpointException {
-        Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString()));
-        try {
-            if (logAllRequests) {
-                LOGGER.info("[SCIM] Sending UPDATE " + scimForReplace.toPrettyString() + "\n to " + getScimEndpoint());
-            }
-            ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
-                    .update(getResourceClass(), getScimEndpoint(), externalId.asString())
-                    .setResource(scimForReplace)
-                    .sendRequest()
-            );
-            checkResponseIsSuccess(response);
-        } catch (Exception e) {
-            LOGGER.warn(e);
-            throw new InvalidResponseFromScimEndpointException("Exception while retrying update " + e.getMessage(), e);
-        }
-    }
-
-    public void delete(EntityOnRemoteScimId externalId) throws InvalidResponseFromScimEndpointException {
-        Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString()));
-        if (logAllRequests) {
-            LOGGER.info("[SCIM] Sending DELETE to " + getScimEndpoint());
-        }
-        try {
-            ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
-                    .delete(getResourceClass(), getScimEndpoint(), externalId.asString())
-                    .sendRequest()
-            );
-            checkResponseIsSuccess(response);
-        } catch (Exception e) {
-            LOGGER.warn(e);
-            throw new InvalidResponseFromScimEndpointException("Exception while retrying delete " + e.getMessage(), e);
-        }
-    }
-
-    @Override
-    public void close() {
-        scimRequestBuilder.close();
-    }
-
-    public List<S> listResources() {
-        ServerResponse<ListResponse<S>> response = scimRequestBuilder.list(getResourceClass(), getScimEndpoint()).get().sendRequest();
-        ListResponse<S> resourceTypeListResponse = response.getResource();
-        return resourceTypeListResponse.getListedResources();
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/service/ScimResourceType.java b/src/main/java/sh/libre/scim/core/service/ScimResourceType.java
deleted file mode 100644
index b90845b6cfd63852c41218e40222a24a639cf13b..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/ScimResourceType.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package sh.libre.scim.core.service;
-
-import de.captaingoldfish.scim.sdk.common.resources.Group;
-import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
-import de.captaingoldfish.scim.sdk.common.resources.User;
-
-public enum ScimResourceType {
-
-    USER("/Users", User.class),
-
-    GROUP("/Groups", Group.class);
-
-    private final String endpoint;
-
-    private final Class<? extends ResourceNode> resourceClass;
-
-    ScimResourceType(String endpoint, Class<? extends ResourceNode> resourceClass) {
-        this.endpoint = endpoint;
-        this.resourceClass = resourceClass;
-    }
-
-    public String getEndpoint() {
-        return endpoint;
-    }
-
-    public <T extends ResourceNode> Class<T> getResourceClass() {
-        return (Class<T>) resourceClass;
-    }
-}
diff --git a/src/main/java/sh/libre/scim/core/service/UserScimService.java b/src/main/java/sh/libre/scim/core/service/UserScimService.java
deleted file mode 100644
index c0262f3d4aeb0b8fca2ea1c397850d1c845c7a8d..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/core/service/UserScimService.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package sh.libre.scim.core.service;
-
-import de.captaingoldfish.scim.sdk.common.resources.User;
-import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
-import de.captaingoldfish.scim.sdk.common.resources.complex.Name;
-import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Email;
-import de.captaingoldfish.scim.sdk.common.resources.multicomplex.MultiComplexNode;
-import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
-import org.apache.commons.lang3.BooleanUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.jboss.logging.Logger;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RoleMapperModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
-import sh.libre.scim.core.ScrimEndPointConfiguration;
-import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
-import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
-import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-public class UserScimService extends AbstractScimService<UserModel, User> {
-    private static final Logger LOGGER = Logger.getLogger(UserScimService.class);
-
-    public UserScimService(
-            KeycloakSession keycloakSession,
-            ScrimEndPointConfiguration scimProviderConfiguration,
-            SkipOrStopStrategy skipOrStopStrategy) {
-        super(keycloakSession, scimProviderConfiguration, ScimResourceType.USER, skipOrStopStrategy);
-    }
-
-    @Override
-    protected Stream<UserModel> getResourceStream() {
-        return getKeycloakDao().getUsersStream();
-    }
-
-    @Override
-    protected boolean entityExists(KeycloakId keycloakId) {
-        return getKeycloakDao().userExists(keycloakId);
-    }
-
-    @Override
-    protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(User resource) throws InconsistentScimMappingException {
-        Optional<KeycloakId> matchedByUsername = resource.getUserName()
-                .map(getKeycloakDao()::getUserByUsername)
-                .map(this::getId);
-        Optional<KeycloakId> matchedByEmail = resource.getEmails().stream()
-                .findFirst()
-                .flatMap(MultiComplexNode::getValue)
-                .map(getKeycloakDao()::getUserByEmail)
-                .map(this::getId);
-        if (matchedByUsername.isPresent()
-            && matchedByEmail.isPresent()
-            && !matchedByUsername.equals(matchedByEmail)) {
-            String inconstencyErrorMessage = "Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get();
-            LOGGER.warn(inconstencyErrorMessage);
-            throw new InconsistentScimMappingException(inconstencyErrorMessage);
-        }
-        if (matchedByUsername.isPresent()) {
-            return matchedByUsername;
-        }
-        return matchedByEmail;
-    }
-
-    @Override
-    protected KeycloakId createEntity(User resource) throws UnexpectedScimDataException {
-        String username = resource.getUserName()
-                .filter(StringUtils::isNotBlank)
-                .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim user has empty username, can't create. Resource id = %s".formatted(resource.getId())));
-        UserModel user = getKeycloakDao().addUser(username);
-        resource.getEmails().stream()
-                .findFirst()
-                .flatMap(MultiComplexNode::getValue)
-                .ifPresent(user::setEmail);
-        boolean userEnabled = resource.isActive().orElse(false);
-        user.setEnabled(userEnabled);
-        return new KeycloakId(user.getId());
-    }
-
-    @Override
-    protected boolean isMarkedToIgnore(UserModel userModel) {
-        return BooleanUtils.TRUE.equals(userModel.getFirstAttribute("scim-skip"));
-    }
-
-    @Override
-    protected KeycloakId getId(UserModel userModel) {
-        return new KeycloakId(userModel.getId());
-    }
-
-    @Override
-    protected User scimRequestBodyForCreate(UserModel roleMapperModel) {
-        String firstAndLastName = String.format("%s %s",
-                StringUtils.defaultString(roleMapperModel.getFirstName()),
-                StringUtils.defaultString(roleMapperModel.getLastName())).trim();
-        String displayName = Objects.toString(firstAndLastName, roleMapperModel.getUsername());
-        Stream<RoleModel> groupRoleModels = roleMapperModel.getGroupsStream().flatMap(RoleMapperModel::getRoleMappingsStream);
-        Stream<RoleModel> roleModels = roleMapperModel.getRoleMappingsStream();
-        Stream<RoleModel> allRoleModels = Stream.concat(groupRoleModels, roleModels);
-        List<PersonRole> roles = allRoleModels
-                .filter(r -> BooleanUtils.TRUE.equals(r.getFirstAttribute("scim")))
-                .map(RoleModel::getName)
-                .map(roleName -> {
-                    PersonRole personRole = new PersonRole();
-                    personRole.setValue(roleName);
-                    return personRole;
-                })
-                .toList();
-        User user = new User();
-        user.setRoles(roles);
-        user.setExternalId(roleMapperModel.getId());
-        user.setUserName(roleMapperModel.getUsername());
-        user.setDisplayName(displayName);
-        Name name = new Name();
-        name.setFamilyName(roleMapperModel.getLastName());
-        name.setGivenName(roleMapperModel.getFirstName());
-        user.setName(name);
-        List<Email> emails = new ArrayList<>();
-        if (roleMapperModel.getEmail() != null) {
-            emails.add(
-                    Email.builder().value(roleMapperModel.getEmail()).build());
-        }
-        user.setEmails(emails);
-        user.setActive(roleMapperModel.isEnabled());
-        return user;
-    }
-
-    @Override
-    protected User scimRequestBodyForUpdate(UserModel userModel, EntityOnRemoteScimId externalId) {
-        User user = scimRequestBodyForCreate(userModel);
-        user.setId(externalId.asString());
-        Meta meta = newMetaLocation(externalId);
-        user.setMeta(meta);
-        return user;
-    }
-
-    @Override
-    protected boolean shouldIgnoreForScimSynchronization(UserModel userModel) {
-        return "admin".equals(userModel.getUsername());
-    }
-}
diff --git a/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java b/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java
deleted file mode 100644
index 4c49f74f671b1679698bc3c5710e5eb6d9b9f171..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package sh.libre.scim.event;
-
-import org.jboss.logging.Logger;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.timer.TimerProvider;
-import sh.libre.scim.core.ScimDispatcher;
-
-import java.time.Duration;
-
-/**
- * In charge of making background checks and sent
- * UPDATE requests from group for which membership information has changed.
- * <p>
- * This is required to avoid immediate group membership updates which could cause
- * to incorrect group members list in case of concurrent group membership changes.
- */
-public class ScimBackgroundGroupMembershipUpdater {
-    public static final String GROUP_DIRTY_SINCE_ATTRIBUTE_NAME = "scim-dirty-since";
-
-    private static final Logger LOGGER = Logger.getLogger(ScimBackgroundGroupMembershipUpdater.class);
-    // Update check loop will run every time this delay has passed
-    private static final long UPDATE_CHECK_DELAY_MS = 2000;
-    // If a group is marked dirty since less that this debounce delay, wait for the next update check loop
-    private static final long DEBOUNCE_DELAY_MS = 1200;
-    private final KeycloakSessionFactory sessionFactory;
-
-    public ScimBackgroundGroupMembershipUpdater(KeycloakSessionFactory sessionFactory) {
-        this.sessionFactory = sessionFactory;
-    }
-
-    public void startBackgroundUpdates() {
-        // Every UPDATE_CHECK_DELAY_MS, check for dirty groups and send updates if required
-        try (KeycloakSession keycloakSession = sessionFactory.create()) {
-            TimerProvider timer = keycloakSession.getProvider(TimerProvider.class);
-            timer.scheduleTask(taskSession -> {
-                for (RealmModel realm : taskSession.realms().getRealmsStream().toList()) {
-                    dispatchDirtyGroupsUpdates(realm);
-                }
-            }, Duration.ofMillis(UPDATE_CHECK_DELAY_MS).toMillis(), "scim-background");
-        }
-    }
-
-    private void dispatchDirtyGroupsUpdates(RealmModel realm) {
-        KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> {
-            session.getContext().setRealm(realm);
-            ScimDispatcher dispatcher = new ScimDispatcher(session);
-            // Identify groups marked as dirty by the ScimEventListenerProvider
-            for (GroupModel group : session.groups().getGroupsStream(realm)
-                    .filter(this::isDirtyGroup).toList()) {
-                LOGGER.infof("[SCIM] Group %s is dirty, dispatch an update", group.getName());
-                // If dirty : dispatch a group update to all clients and mark it clean
-                dispatcher.dispatchGroupModificationToAll(client -> client.update(group));
-                group.removeAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME);
-            }
-            dispatcher.close();
-        });
-    }
-
-    private boolean isDirtyGroup(GroupModel g) {
-        String groupDirtySinceAttribute = g.getFirstAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME);
-        try {
-            long groupDirtySince = Long.parseLong(groupDirtySinceAttribute);
-            // Must be dirty for more than DEBOUNCE_DELAY_MS
-            // (otherwise update will be dispatched in next scheduled loop)
-            return System.currentTimeMillis() - groupDirtySince > DEBOUNCE_DELAY_MS;
-        } catch (NumberFormatException e) {
-            return false;
-        }
-    }
-}
diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java
deleted file mode 100644
index 2c177b0e47d1e7df86de5fe2413ccb114b4d7333..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java
+++ /dev/null
@@ -1,247 +0,0 @@
-package sh.libre.scim.event;
-
-import org.jboss.logging.Logger;
-import org.keycloak.component.ComponentModel;
-import org.keycloak.events.Event;
-import org.keycloak.events.EventListenerProvider;
-import org.keycloak.events.EventType;
-import org.keycloak.events.admin.AdminEvent;
-import org.keycloak.events.admin.OperationType;
-import org.keycloak.events.admin.ResourceType;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.UserModel;
-import sh.libre.scim.core.ScimDispatcher;
-import sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory;
-import sh.libre.scim.core.service.KeycloakDao;
-import sh.libre.scim.core.service.KeycloakId;
-import sh.libre.scim.core.service.ScimResourceType;
-
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
-
-/**
- * An Event listener reacting to Keycloak models  modification
- * (e.g. User creation, Group deletion, membership modifications, endpoint configuration change...)
- * by propagating it to all registered Scim endpoints.
- */
-public class ScimEventListenerProvider implements EventListenerProvider {
-
-    private static final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class);
-
-    private final ScimDispatcher dispatcher;
-
-    private final KeycloakSession session;
-
-    private final KeycloakDao keycloakDao;
-
-    private final Map<ResourceType, Pattern> listenedEventPathPatterns = Map.of(
-            ResourceType.USER, Pattern.compile("users/(.+)"),
-            ResourceType.GROUP, Pattern.compile("groups/([\\w-]+)(/children)?"),
-            ResourceType.GROUP_MEMBERSHIP, Pattern.compile("users/(.+)/groups/(.+)"),
-            ResourceType.REALM_ROLE_MAPPING, Pattern.compile("^(.+)/(.+)/role-mappings"),
-            ResourceType.COMPONENT, Pattern.compile("components/(.+)")
-    );
-
-    public ScimEventListenerProvider(KeycloakSession session) {
-        this.session = session;
-        this.keycloakDao = new KeycloakDao(session);
-        this.dispatcher = new ScimDispatcher(session);
-    }
-
-    @Override
-    public void onEvent(Event event) {
-        // React to User-related event : creation, deletion, update
-        EventType eventType = event.getType();
-        KeycloakId eventUserId = new KeycloakId(event.getUserId());
-        switch (eventType) {
-            case REGISTER -> {
-                LOGGER.infof("[SCIM] Propagate User Registration - %s", eventUserId);
-                UserModel user = getUser(eventUserId);
-                dispatcher.dispatchUserModificationToAll(client -> client.create(user));
-            }
-            case UPDATE_EMAIL, UPDATE_PROFILE -> {
-                LOGGER.infof("[SCIM] Propagate User %s - %s", eventType, eventUserId);
-                UserModel user = getUser(eventUserId);
-                dispatcher.dispatchUserModificationToAll(client -> client.update(user));
-            }
-            case DELETE_ACCOUNT -> {
-                LOGGER.infof("[SCIM] Propagate User deletion - %s", eventUserId);
-                dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId));
-            }
-            default -> {
-                // No other event has to be propagated to Scim endpoints
-            }
-        }
-    }
-
-
-    @Override
-    public void onEvent(AdminEvent event, boolean includeRepresentation) {
-        // Step 1: check if event is relevant for propagation through SCIM
-        Pattern pattern = listenedEventPathPatterns.get(event.getResourceType());
-        if (pattern == null)
-            return;
-        Matcher matcher = pattern.matcher(event.getResourcePath());
-        if (!matcher.find())
-            return;
-
-
-        // Step 2: propagate event (if needed) according to its resource type
-        switch (event.getResourceType()) {
-            case USER -> {
-                KeycloakId userId = new KeycloakId(matcher.group(1));
-                handleUserEvent(event, userId);
-            }
-            case GROUP -> {
-                KeycloakId groupId = new KeycloakId(matcher.group(1));
-                handleGroupEvent(event, groupId);
-            }
-            case GROUP_MEMBERSHIP -> {
-                KeycloakId userId = new KeycloakId(matcher.group(1));
-                KeycloakId groupId = new KeycloakId(matcher.group(2));
-                handleGroupMemberShipEvent(event, userId, groupId);
-            }
-            case REALM_ROLE_MAPPING -> {
-                String rawResourceType = matcher.group(1);
-                ScimResourceType type = switch (rawResourceType) {
-                    case "users" -> ScimResourceType.USER;
-                    case "groups" -> ScimResourceType.GROUP;
-                    default -> throw new IllegalArgumentException("Unsupported resource type: " + rawResourceType);
-                };
-                KeycloakId id = new KeycloakId(matcher.group(2));
-                handleRoleMappingEvent(event, type, id);
-            }
-            case COMPONENT -> {
-                String id = matcher.group(1);
-                handleScimEndpointConfigurationEvent(event, id);
-
-            }
-            default -> {
-                // No other resource modification has to be propagated to Scim endpoints
-            }
-        }
-    }
-
-
-    private void handleUserEvent(AdminEvent userEvent, KeycloakId userId) {
-        LOGGER.infof("[SCIM] Propagate User %s - %s", userEvent.getOperationType(), userId);
-        switch (userEvent.getOperationType()) {
-            case CREATE -> {
-                UserModel user = getUser(userId);
-                dispatcher.dispatchUserModificationToAll(client -> client.create(user));
-                user.getGroupsStream().forEach(group ->
-                        dispatcher.dispatchGroupModificationToAll(client -> client.update(group)
-                        ));
-            }
-            case UPDATE -> {
-                UserModel user = getUser(userId);
-                dispatcher.dispatchUserModificationToAll(client -> client.update(user));
-            }
-            case DELETE -> dispatcher.dispatchUserModificationToAll(client -> client.delete(userId));
-            default -> {
-                // ACTION userEvent are not relevant, nothing to do
-            }
-        }
-    }
-
-    /**
-     * Propagating the given group-related event to Scim endpoints.
-     *
-     * @param event   the event to propagate
-     * @param groupId event target's id
-     */
-    private void handleGroupEvent(AdminEvent event, KeycloakId groupId) {
-        LOGGER.infof("[SCIM] Propagate Group %s - %s", event.getOperationType(), groupId);
-        switch (event.getOperationType()) {
-            case CREATE -> {
-                GroupModel group = getGroup(groupId);
-                dispatcher.dispatchGroupModificationToAll(client -> client.create(group));
-            }
-            case UPDATE -> {
-                GroupModel group = getGroup(groupId);
-                dispatcher.dispatchGroupModificationToAll(client -> client.update(group));
-            }
-            case DELETE -> dispatcher.dispatchGroupModificationToAll(client -> client.delete(groupId));
-            default -> {
-                // ACTION event are not relevant, nothing to do
-            }
-        }
-    }
-
-    private void handleGroupMemberShipEvent(AdminEvent groupMemberShipEvent, KeycloakId userId, KeycloakId groupId) {
-        LOGGER.infof("[SCIM] Propagate GroupMemberShip %s - User %s Group %s", groupMemberShipEvent.getOperationType(), userId, groupId);
-        // Step 1: update USER immediately
-        GroupModel group = getGroup(groupId);
-        UserModel user = getUser(userId);
-        dispatcher.dispatchUserModificationToAll(client -> client.update(user));
-
-        // Step 2: delayed GROUP update :
-        // if several users are added to the group simultaneously in different Keycloack sessions
-        // update the group in the context of the current session may not reflect those other changes
-        // We trigger a delayed update by setting an attribute on the group (that will be handled by ScimBackgroundGroupMembershipUpdaters)
-        group.setSingleAttribute(ScimBackgroundGroupMembershipUpdater.GROUP_DIRTY_SINCE_ATTRIBUTE_NAME, "" + System.currentTimeMillis());
-    }
-
-    private void handleRoleMappingEvent(AdminEvent roleMappingEvent, ScimResourceType type, KeycloakId id) {
-        LOGGER.infof("[SCIM] Propagate RoleMapping %s - %s %s", roleMappingEvent.getOperationType(), type, id);
-        switch (type) {
-            case USER -> {
-                UserModel user = getUser(id);
-                dispatcher.dispatchUserModificationToAll(client -> client.update(user));
-            }
-            case GROUP -> {
-                GroupModel group = getGroup(id);
-                session.users()
-                        .getGroupMembersStream(session.getContext().getRealm(), group)
-                        .forEach(user ->
-                                dispatcher.dispatchUserModificationToAll(client -> client.update(user)
-                                ));
-            }
-            default -> {
-                // No other type is relevant for propagation
-            }
-        }
-    }
-
-    private void handleScimEndpointConfigurationEvent(AdminEvent event, String id) {
-        // In case of a component deletion
-        if (event.getOperationType() == OperationType.DELETE) {
-            // Check if it was a Scim endpoint configuration, and forward deletion if so
-            Stream<ComponentModel> scimEndpointConfigurationsWithDeletedId = session.getContext().getRealm().getComponentsStream()
-                    .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId())
-                                 && id.equals(m.getId()));
-            if (scimEndpointConfigurationsWithDeletedId.iterator().hasNext()) {
-                LOGGER.infof("[SCIM] SCIM Endpoint configuration DELETE - %s ", id);
-                dispatcher.refreshActiveScimEndpoints();
-            }
-        } else {
-            // In case of CREATE or UPDATE, we can directly use the string representation
-            // to check if it defines a SCIM endpoint (faster)
-            if (event.getRepresentation() != null
-                && event.getRepresentation().contains("\"providerId\":\"scim\"")) {
-                LOGGER.infof("[SCIM] SCIM Endpoint configuration CREATE - %s ", id);
-                dispatcher.refreshActiveScimEndpoints();
-            }
-        }
-
-    }
-
-
-    private UserModel getUser(KeycloakId id) {
-        return keycloakDao.getUserById(id);
-    }
-
-    private GroupModel getGroup(KeycloakId id) {
-        return keycloakDao.getGroupById(id);
-    }
-
-    @Override
-    public void close() {
-        dispatcher.close();
-    }
-
-
-}
diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java
deleted file mode 100644
index c7b437a287dcb0aa1001a134aad3749505b06925..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package sh.libre.scim.event;
-
-import org.keycloak.Config.Scope;
-import org.keycloak.events.EventListenerProvider;
-import org.keycloak.events.EventListenerProviderFactory;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-
-public class ScimEventListenerProviderFactory implements EventListenerProviderFactory {
-
-    @Override
-    public EventListenerProvider create(KeycloakSession session) {
-        return new ScimEventListenerProvider(session);
-    }
-
-    @Override
-    public String getId() {
-        return "scim";
-    }
-
-    @Override
-    public void init(Scope config) {
-        // Nothing to initialize
-    }
-
-    @Override
-    public void postInit(KeycloakSessionFactory factory) {
-        // Nothing to initialize
-    }
-
-    @Override
-    public void close() {
-        // Nothing to close
-    }
-
-}
diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java b/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java
deleted file mode 100644
index 4deec373b952c59c333974b645fa276b9cc37d4a..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package sh.libre.scim.jpa;
-
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.NoResultException;
-import jakarta.persistence.TypedQuery;
-import org.keycloak.connections.jpa.JpaConnectionProvider;
-import org.keycloak.models.KeycloakSession;
-import sh.libre.scim.core.service.EntityOnRemoteScimId;
-import sh.libre.scim.core.service.KeycloakId;
-import sh.libre.scim.core.service.ScimResourceType;
-
-import java.util.Optional;
-
-public class ScimResourceDao {
-
-    private final String realmId;
-
-    private final String componentId;
-
-    private final EntityManager entityManager;
-
-    private ScimResourceDao(String realmId, String componentId, EntityManager entityManager) {
-        this.realmId = realmId;
-        this.componentId = componentId;
-        this.entityManager = entityManager;
-    }
-
-    public static ScimResourceDao newInstance(KeycloakSession keycloakSession, String componentId) {
-        String realmId = keycloakSession.getContext().getRealm().getId();
-        EntityManager entityManager = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager();
-        return new ScimResourceDao(realmId, componentId, entityManager);
-    }
-
-    private EntityManager getEntityManager() {
-        return entityManager;
-    }
-
-    private String getRealmId() {
-        return realmId;
-    }
-
-    private String getComponentId() {
-        return componentId;
-    }
-
-    public void create(KeycloakId id, EntityOnRemoteScimId externalId, ScimResourceType type) {
-        ScimResourceMapping entity = new ScimResourceMapping();
-        entity.setType(type.name());
-        entity.setExternalId(externalId.asString());
-        entity.setComponentId(componentId);
-        entity.setRealmId(realmId);
-        entity.setId(id.asString());
-        entityManager.persist(entity);
-    }
-
-    private TypedQuery<ScimResourceMapping> getScimResourceTypedQuery(String queryName, String id, ScimResourceType type) {
-        return getEntityManager()
-                .createNamedQuery(queryName, ScimResourceMapping.class)
-                .setParameter("type", type.name())
-                .setParameter("realmId", getRealmId())
-                .setParameter("componentId", getComponentId())
-                .setParameter("id", id);
-    }
-
-    public Optional<ScimResourceMapping> findByExternalId(EntityOnRemoteScimId externalId, ScimResourceType type) {
-        try {
-            return Optional.of(
-                    getScimResourceTypedQuery("findByExternalId", externalId.asString(), type).getSingleResult()
-            );
-        } catch (NoResultException e) {
-            return Optional.empty();
-        }
-    }
-
-    public Optional<ScimResourceMapping> findById(KeycloakId keycloakId, ScimResourceType type) {
-        try {
-            return Optional.of(
-                    getScimResourceTypedQuery("findById", keycloakId.asString(), type).getSingleResult()
-            );
-        } catch (NoResultException e) {
-            return Optional.empty();
-        }
-    }
-
-    public Optional<ScimResourceMapping> findUserById(KeycloakId id) {
-        return findById(id, ScimResourceType.USER);
-    }
-
-    public Optional<ScimResourceMapping> findUserByExternalId(EntityOnRemoteScimId externalId) {
-        return findByExternalId(externalId, ScimResourceType.USER);
-    }
-
-    public void delete(ScimResourceMapping resource) {
-        entityManager.remove(resource);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java
deleted file mode 100644
index d0abddf2b1bf9c93c473f94af1dbf061afbb92f6..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package sh.libre.scim.jpa;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.Serializable;
-import java.util.Objects;
-
-public class ScimResourceId implements Serializable {
-    private String id;
-    private String realmId;
-    private String componentId;
-    private String type;
-    private String externalId;
-
-    public ScimResourceId() {
-    }
-
-    public ScimResourceId(String id, String realmId, String componentId, String type, String externalId) {
-        this.setId(id);
-        this.setRealmId(realmId);
-        this.setComponentId(componentId);
-        this.setType(type);
-        this.setExternalId(externalId);
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getRealmId() {
-        return realmId;
-    }
-
-    public void setRealmId(String realmId) {
-        this.realmId = realmId;
-    }
-
-    public String getComponentId() {
-        return componentId;
-    }
-
-    public void setComponentId(String componentId) {
-        this.componentId = componentId;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public void setType(String type) {
-        this.type = type;
-    }
-
-    public String getExternalId() {
-        return externalId;
-    }
-
-    public void setExternalId(String externalId) {
-        this.externalId = externalId;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (this == other)
-            return true;
-        if (!(other instanceof ScimResourceId o))
-            return false;
-        return (StringUtils.equals(o.id, id) &&
-                StringUtils.equals(o.realmId, realmId) &&
-                StringUtils.equals(o.componentId, componentId) &&
-                StringUtils.equals(o.type, type) &&
-                StringUtils.equals(o.externalId, externalId));
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(realmId, componentId, type, id, externalId);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java b/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java
deleted file mode 100644
index ade6848ccd2e8aea7df643bb78554e375a166b68..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package sh.libre.scim.jpa;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.IdClass;
-import jakarta.persistence.NamedQueries;
-import jakarta.persistence.NamedQuery;
-import jakarta.persistence.Table;
-import sh.libre.scim.core.service.EntityOnRemoteScimId;
-import sh.libre.scim.core.service.KeycloakId;
-
-@Entity
-@IdClass(ScimResourceId.class)
-@Table(name = "SCIM_RESOURCE_MAPPING")
-@NamedQueries({
-        @NamedQuery(name = "findById", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and id = :id"),
-        @NamedQuery(name = "findByExternalId", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id")
-})
-public class ScimResourceMapping {
-
-    @Id
-    @Column(name = "ID", nullable = false)
-    private String id;
-
-    @Id
-    @Column(name = "REALM_ID", nullable = false)
-    private String realmId;
-
-    @Id
-    @Column(name = "COMPONENT_ID", nullable = false)
-    private String componentId;
-
-    @Id
-    @Column(name = "TYPE", nullable = false)
-    private String type;
-
-    @Id
-    @Column(name = "EXTERNAL_ID", nullable = false)
-    private String externalId;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getRealmId() {
-        return realmId;
-    }
-
-    public void setRealmId(String realmId) {
-        this.realmId = realmId;
-    }
-
-    public String getComponentId() {
-        return componentId;
-    }
-
-    public void setComponentId(String componentId) {
-        this.componentId = componentId;
-    }
-
-    public String getExternalId() {
-        return externalId;
-    }
-
-    public void setExternalId(String externalId) {
-        this.externalId = externalId;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public void setType(String type) {
-        this.type = type;
-    }
-
-    public KeycloakId getIdAsKeycloakId() {
-        return new KeycloakId(id);
-    }
-
-    public EntityOnRemoteScimId getExternalIdAsEntityOnRemoteScimId() {
-        return new EntityOnRemoteScimId(externalId);
-    }
-}
diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java
deleted file mode 100644
index 6ef55a060e10945b5eb017559ea9b9c794122381..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package sh.libre.scim.jpa;
-
-import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
-
-import java.util.Collections;
-import java.util.List;
-
-public class ScimResourceProvider implements JpaEntityProvider {
-
-    @Override
-    public List<Class<?>> getEntities() {
-        return Collections.singletonList(ScimResourceMapping.class);
-    }
-
-    @Override
-    public String getChangelogLocation() {
-        return "META-INF/scim-resource-changelog.xml";
-    }
-
-    @Override
-    public void close() {
-        // Nothing to close
-    }
-
-    @Override
-    public String getFactoryId() {
-        return ScimResourceProviderFactory.ID;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java
deleted file mode 100644
index 7f3cc323ddf72b0169f1c61a34bdff66ad4ef858..0000000000000000000000000000000000000000
--- a/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package sh.libre.scim.jpa;
-
-import org.keycloak.Config.Scope;
-import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
-import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-
-public class ScimResourceProviderFactory implements JpaEntityProviderFactory {
-
-    static final String ID = "scim-resource";
-
-    @Override
-    public JpaEntityProvider create(KeycloakSession session) {
-        return new ScimResourceProvider();
-    }
-
-    @Override
-    public String getId() {
-        return ID;
-    }
-
-    @Override
-    public void init(Scope scope) {
-        // Nothing to initialise
-    }
-
-    @Override
-    public void postInit(KeycloakSessionFactory sessionFactory) {
-        // Nothing to do
-    }
-
-
-    @Override
-    public void close() {
-        // Nothing to close
-    }
-
-}
diff --git a/src/main/resources/META-INF/jboss-deployment-structure.xml b/src/main/resources/META-INF/jboss-deployment-structure.xml
deleted file mode 100644
index 42007fe63830cbd5550ac2b55c43ce51c4ec377c..0000000000000000000000000000000000000000
--- a/src/main/resources/META-INF/jboss-deployment-structure.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<jboss-deployment-structure>
-    <deployment>
-        <dependencies>
-            <module name="org.keycloak.keycloak-services" />
-            <module name="org.keycloak.keycloak-model-jpa" />
-            <module name="org.hibernate" />
-        </dependencies>
-    </deployment>
-</jboss-deployment-structure>
diff --git a/src/main/resources/META-INF/scim-resource-changelog.xml b/src/main/resources/META-INF/scim-resource-changelog.xml
deleted file mode 100644
index d3e2687a57b42ea71b1f9e2bb848b9c4da56cca4..0000000000000000000000000000000000000000
--- a/src/main/resources/META-INF/scim-resource-changelog.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
-                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
-    <changeSet author="contact@indiehosters.net" id="scim-resource-1.0">
-
-        <createTable tableName="SCIM_RESOURCE_MAPPING">
-            <column name="ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="REALM_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="TYPE" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="COMPONENT_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="EXTERNAL_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-        </createTable>
-
-        <addPrimaryKey constraintName="PK_SCIM_RESOURCE_MAPPING" tableName="SCIM_RESOURCE_MAPPING"
-                       columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID"/>
-        <addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="REALM_ID"
-                                 constraintName="FK_SCIM_RESOURCE_MAPPING_REALM" referencedTableName="REALM"
-                                 referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
-        <addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="COMPONENT_ID"
-                                 constraintName="FK_SCIM_RESOURCE_MAPPING_COMPONENT" referencedTableName="COMPONENT"
-                                 referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
-    </changeSet>
-
-</databaseChangeLog>
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
deleted file mode 100644
index b3cb1a13e34435fc9114e7cd02ab77d87b3c6871..0000000000000000000000000000000000000000
--- a/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
+++ /dev/null
@@ -1 +0,0 @@
-sh.libre.scim.jpa.ScimResourceProviderFactory
diff --git a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
deleted file mode 100644
index 7e2a6edd9cccb0c544c7d6b9fbd65ffc3ff7324a..0000000000000000000000000000000000000000
--- a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
+++ /dev/null
@@ -1 +0,0 @@
-sh.libre.scim.event.ScimEventListenerProviderFactory
diff --git a/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
deleted file mode 100644
index 308796c862e2aa83c0aa9212cb123128a749bd1e..0000000000000000000000000000000000000000
--- a/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
+++ /dev/null
@@ -1 +0,0 @@
-sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory