diff --git a/src/main/java/sh/libre/scim/core/ScrimProviderConfiguration.java b/src/main/java/sh/libre/scim/core/ScrimProviderConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..c68938511ba7ad520b3511b09df4ce316e2c1584
--- /dev/null
+++ b/src/main/java/sh/libre/scim/core/ScrimProviderConfiguration.java
@@ -0,0 +1,77 @@
+package sh.libre.scim.core;
+
+import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
+import org.keycloak.component.ComponentModel;
+
+public class ScrimProviderConfiguration {
+
+    private final String endPoint;
+    private final String id;
+    private final String contentType;
+    private final String authorizationHeaderValue;
+    private final ImportAction importAction;
+    private final boolean syncImport;
+    private final boolean syncRefresh;
+
+    public ScrimProviderConfiguration(ComponentModel scimProviderConfiguration) {
+        AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get("auth-mode"));
+        authorizationHeaderValue = switch (authMode) {
+            case BEARER -> "Bearer " + scimProviderConfiguration.get("auth-pass");
+            case BASIC_AUTH -> {
+                BasicAuth basicAuth = BasicAuth.builder()
+                        .username(scimProviderConfiguration.get("auth-user"))
+                        .password(scimProviderConfiguration.get("auth-pass"))
+                        .build();
+                yield basicAuth.getAuthorizationHeaderValue();
+            }
+            default ->
+                    throw new IllegalArgumentException("authMode " + scimProviderConfiguration + " is not supported");
+        };
+        contentType = scimProviderConfiguration.get("content-type");
+        endPoint = scimProviderConfiguration.get("endpoint");
+        id = scimProviderConfiguration.getId();
+        importAction = ImportAction.valueOf(scimProviderConfiguration.get("sync-import-action"));
+        syncImport = scimProviderConfiguration.get("sync-import", false);
+        syncRefresh = scimProviderConfiguration.get("sync-refresh", false);
+    }
+
+    public boolean isSyncRefresh() {
+        return syncRefresh;
+    }
+
+    public boolean isSyncImport() {
+        return syncImport;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public String getAuthorizationHeaderValue() {
+        return authorizationHeaderValue;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public ImportAction getImportAction() {
+        return importAction;
+    }
+
+    public String getEndPoint() {
+        return endPoint;
+    }
+
+    public enum AuthMode {
+        BEARER, BASIC_AUTH, NONE
+    }
+
+    public enum EndpointContentType {
+        JSON, SCIM_JSON
+    }
+
+    public enum ImportAction {
+        CREATE_LOCAL, DELETE_REMOTE, NOTHING
+    }
+}
diff --git a/src/main/java/sh/libre/scim/core/UserScimClient.java b/src/main/java/sh/libre/scim/core/UserScimClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..789d852d6811143c658429f9e051a7b3da36ba66
--- /dev/null
+++ b/src/main/java/sh/libre/scim/core/UserScimClient.java
@@ -0,0 +1,279 @@
+package sh.libre.scim.core;
+
+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.exceptions.ResponseException;
+import de.captaingoldfish.scim.sdk.common.resources.User;
+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.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.ws.rs.ProcessingException;
+import org.jboss.logging.Logger;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserModel;
+import org.keycloak.storage.user.SynchronizationResult;
+import sh.libre.scim.jpa.ScimResource;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UserScimClient implements AutoCloseable {
+
+    private static final Logger LOGGER = Logger.getLogger(UserScimClient.class);
+
+    private final ScimRequestBuilder scimRequestBuilder;
+
+    private final RetryRegistry retryRegistry;
+
+    private final KeycloakSession keycloakSession;
+
+    private final ScrimProviderConfiguration scimProviderConfiguration;
+
+    /**
+     * Builds a new {@link UserScimClient}
+     *
+     * @param scimRequestBuilder
+     * @param retryRegistry             Retry policy to use
+     * @param keycloakSession
+     * @param scimProviderConfiguration
+     */
+    private UserScimClient(ScimRequestBuilder scimRequestBuilder, RetryRegistry retryRegistry, KeycloakSession keycloakSession, ScrimProviderConfiguration scimProviderConfiguration) {
+        this.scimRequestBuilder = scimRequestBuilder;
+        this.retryRegistry = retryRegistry;
+        this.keycloakSession = keycloakSession;
+        this.scimProviderConfiguration = scimProviderConfiguration;
+    }
+
+
+    public static UserScimClient newUserScimClient(ComponentModel componentModel, KeycloakSession session) {
+        ScrimProviderConfiguration scimProviderConfiguration = new ScrimProviderConfiguration(componentModel);
+        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)
+                .expectedHttpResponseHeaders(Collections.emptyMap()) // strange, useful?
+                // TODO Question Indiehoster : should we really allow connection with TLS ? .hostnameVerifier((s, sslSession) -> true)
+                .build();
+
+        String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint();
+        ScimRequestBuilder scimRequestBuilder =
+                new ScimRequestBuilder(
+                        scimApplicationBaseUrl,
+                        scimClientConfig
+                );
+
+        RetryConfig retryConfig = RetryConfig.custom()
+                .maxAttempts(10)
+                .intervalFunction(IntervalFunction.ofExponentialBackoff())
+                .retryExceptions(ProcessingException.class)
+                .build();
+
+        RetryRegistry retryRegistry = RetryRegistry.of(retryConfig);
+        return new UserScimClient(scimRequestBuilder, retryRegistry, session, scimProviderConfiguration);
+    }
+
+    public void create(UserModel userModel) {
+        UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+        adapter.apply(userModel);
+        if (adapter.skip)
+            return;
+        // If mapping exist then it was created by import so skip.
+        if (adapter.query("findById", adapter.getId()).getResultList().isEmpty()) {
+            return;
+        }
+        Retry retry = retryRegistry.retry("create-" + adapter.getId());
+        ServerResponse<User> response = retry.executeSupplier(() -> {
+            try {
+                return scimRequestBuilder
+                        .create(adapter.getResourceClass(), adapter.getScimEndpoint())
+                        .setResource(adapter.toScim())
+                        .sendRequest();
+            } catch (ResponseException e) {
+                throw new RuntimeException(e);
+            }
+        });
+
+        if (!response.isSuccess()) {
+            LOGGER.warn(response.getResponseBody());
+            LOGGER.warn(response.getHttpStatus());
+        }
+
+        adapter.apply(response.getResource());
+        adapter.saveMapping();
+    }
+
+    public void replace(UserModel userModel) {
+        UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+        try {
+            adapter.apply(userModel);
+            if (adapter.skip)
+                return;
+            ScimResource resource = adapter.query("findById", adapter.getId()).getSingleResult();
+            adapter.apply(resource);
+            Retry retry = retryRegistry.retry("replace-" + adapter.getId());
+            ServerResponse<User> response = retry.executeSupplier(() -> {
+                try {
+                    return scimRequestBuilder
+                            .update(adapter.getResourceClass(), adapter.getScimEndpoint(), adapter.getExternalId())
+                            .setResource(adapter.toScim())
+                            .sendRequest();
+                } catch (ResponseException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            if (!response.isSuccess()) {
+                LOGGER.warn(response.getResponseBody());
+                LOGGER.warn(response.getHttpStatus());
+            }
+        } catch (NoResultException e) {
+            LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId());
+        } catch (Exception e) {
+            LOGGER.error(e);
+        }
+    }
+
+    public void delete(String id) {
+        UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+        adapter.setId(id);
+
+        try {
+            ScimResource resource = adapter.query("findById", adapter.getId()).getSingleResult();
+            adapter.apply(resource);
+
+            Retry retry = retryRegistry.retry("delete-" + id);
+            ServerResponse<User> response = retry.executeSupplier(() -> {
+                try {
+                    return scimRequestBuilder.delete(adapter.getResourceClass(), adapter.getScimEndpoint(), adapter.getExternalId())
+                            .sendRequest();
+                } catch (ResponseException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+
+            if (!response.isSuccess()) {
+                LOGGER.warn(response.getResponseBody());
+                LOGGER.warn(response.getHttpStatus());
+            }
+            EntityManager entityManager = this.keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager();
+            entityManager.remove(resource);
+        } catch (NoResultException e) {
+            LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id);
+        }
+    }
+
+    public void refreshResources(
+            SynchronizationResult syncRes) {
+        LOGGER.info("Refresh resources");
+        UserAdapter a = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+        a.getResourceStream().forEach(resource -> {
+            UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+            adapter.apply(resource);
+            LOGGER.infof("Reconciling local resource %s", adapter.getId());
+            if (!adapter.skipRefresh()) {
+                ScimResource mapping = adapter.getMapping();
+                if (mapping == null) {
+                    LOGGER.info("Creating it");
+                    this.create(resource);
+                } else {
+                    LOGGER.info("Replacing it");
+                    this.replace(resource);
+                }
+                syncRes.increaseUpdated();
+            }
+        });
+
+    }
+
+    public void importResources(SynchronizationResult syncRes) {
+        LOGGER.info("Import");
+        try {
+            UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+            ServerResponse<ListResponse<User>> response = scimRequestBuilder.list(adapter.getResourceClass(), adapter.getScimEndpoint()).get().sendRequest();
+            ListResponse<User> resourceTypeListResponse = response.getResource();
+
+            for (User resource : resourceTypeListResponse.getListedResources()) {
+                try {
+                    LOGGER.infof("Reconciling remote resource %s", resource);
+                    adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
+                    adapter.apply(resource);
+
+                    ScimResource mapping = adapter.getMapping();
+                    if (mapping != null) {
+                        adapter.apply(mapping);
+                        if (adapter.entityExists()) {
+                            LOGGER.info("Valid mapping found, skipping");
+                            continue;
+                        } else {
+                            LOGGER.info("Delete a dangling mapping");
+                            adapter.deleteMapping();
+                        }
+                    }
+
+                    Boolean mapped = adapter.tryToMap();
+                    if (mapped) {
+                        LOGGER.info("Matched");
+                        adapter.saveMapping();
+                    } else {
+                        switch (this.scimProviderConfiguration.getImportAction()) {
+                            case CREATE_LOCAL:
+                                LOGGER.info("Create local resource");
+                                try {
+                                    adapter.createEntity();
+                                    adapter.saveMapping();
+                                    syncRes.increaseAdded();
+                                } catch (Exception e) {
+                                    LOGGER.error(e);
+                                }
+                                break;
+                            case DELETE_REMOTE:
+                                LOGGER.info("Delete remote resource");
+                                scimRequestBuilder
+                                        .delete(adapter.getResourceClass(), adapter.getScimEndpoint(), resource.getId().get())
+                                        .sendRequest();
+                                syncRes.increaseRemoved();
+                                break;
+                            case NOTHING:
+                                LOGGER.info("Import action set to NOTHING");
+                                break;
+                        }
+                    }
+                } catch (Exception e) {
+                    LOGGER.error(e);
+                    e.printStackTrace();
+                    syncRes.increaseFailed();
+                }
+            }
+        } catch (ResponseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void sync(SynchronizationResult syncRes) {
+        if (this.scimProviderConfiguration.isSyncImport()) {
+            this.importResources(syncRes);
+        }
+        if (this.scimProviderConfiguration.isSyncRefresh()) {
+            this.refreshResources(syncRes);
+        }
+    }
+
+
+    @Override
+    public void close() {
+        scimRequestBuilder.close();
+    }
+}