This is an automated email from the ASF dual-hosted git repository.

jsinovassin pushed a commit to branch UNOMI-v2compat-default-tenant
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 81ad9fac3715a0289998b41979dd56cb9d347fcb
Author: jsinovassin <[email protected]>
AuthorDate: Tue Sep 15 12:14:05 2026 +0200

    UNOMI-980: create the compatibility tenant, and name the mode for what it 
does
    
    The compatibility mode lets a client written before Unomi 3.1 keep working 
with
    no change on its side. It refused every request until an operator created a
    tenant by hand, which is a step no earlier version asked for:
    
      ERROR AuthenticationFilter - V2 compatibility mode: configured default 
tenant
      'default' does not exist
    
    Create the tenant the mode runs on. The new TenantService.getOrCreateTenant 
is
    synchronized, and it re-reads the tenant when a concurrent creation wins the
    race. Several nodes starting together all get the tenant, and only one 
creation
    happens.
    
    Drop the tenant identifier setting. The mode read the setting in two 
places, and
    both sit inside the method that runs only when the mode is on. A client from
    before 3.1 knows no tenant, so it never names one, and nothing outside the 
filter
    read the value. The identifier is now a constant.
    
    Two unreachable branches go with the setting. Each call site tested the 
value for
    blankness, while modified() already fell back to "default". The two 
branches also
    disagreed: a blank value denied every public request on one path, and 
switched to
    the system context on the other.
    
    Rename the mode from V2 compatibility to single-tenant compatibility. Unomi 
3.0.0
    and 3.0.1 carry no tenant either, so a 3.0.1 client meets the same wall as 
a V2
    client, and the old name named the wrong thing. The setting becomes
    singletenantcompatibility.enabled.
    
    V2CompatibilityModeIT becomes SingleTenantCompatibilityModeIT. Its two 
tests that
    existed only to exercise the removed setting are gone. testV2ModeBehavior 
fetched
    the profile setUp had created through profileService, which belongs to the 
tenant
    BaseIT works in, so it now reads back the profile its own V2-style request
    created. The write and the read both go through the compatibility path.
---
 .../apache/unomi/api/tenants/TenantService.java    |  12 ++
 .../test/java/org/apache/unomi/itests/AllITs.java  |   2 +-
 .../apache/unomi/itests/CorePersistenceITs.java    |   2 +-
 ...T.java => SingleTenantCompatibilityModeIT.java} | 164 +++++++--------------
 .../asciidoc/migrations/migrate-3.0-to-3.1.adoc    |  16 +-
 .../asciidoc/migrations/v2-compatibility-mode.adoc |  68 ++++-----
 manual/src/main/asciidoc/whats-new.adoc            |   2 +-
 .../main/resources/etc/custom.system.properties    |   3 +-
 .../rest/authentication/AuthenticationFilter.java  |  93 ++++++------
 .../authentication/RestAuthenticationConfig.java   |  13 +-
 .../impl/DefaultRestAuthenticationConfig.java      |  52 ++-----
 .../rest/service/impl/RestServiceUtilsImpl.java    |  16 +-
 .../org.apache.unomi.rest.authentication.cfg       |   7 +-
 .../AuthenticationFilterBlankPasswordTest.java     |  17 +--
 .../services/impl/tenants/TenantServiceImpl.java   |  19 +++
 .../unomi/services/impl/TestTenantService.java     |   6 +
 16 files changed, 217 insertions(+), 275 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java 
b/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
index a5fdb0ab5..dff0128b2 100644
--- a/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
+++ b/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
@@ -61,6 +61,18 @@ public interface TenantService {
      * @param tenantId the ID of the tenant to retrieve
      * @return the Tenant object if found, null otherwise
      */
+    /**
+     * Returns the tenant with the given identifier, and creates it when it 
does not exist yet.
+     * <p>
+     * Two callers that race on the same identifier both get the tenant, and 
only one creation
+     * happens.
+     *
+     * @param tenantId   the identifier of the tenant
+     * @param properties the properties to set when the tenant has to be 
created, may be {@code null}
+     * @return the tenant, never {@code null}
+     */
+    Tenant getOrCreateTenant(String tenantId, Map<String, Object> properties);
+
     Tenant getTenant(String tenantId);
 
     /**
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java 
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 89609d825..d867decb5 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -72,7 +72,7 @@ import org.junit.runners.Suite.SuiteClasses;
         GraphQLProfileAliasesIT.class,
         SendEventActionIT.class,
         ScopeIT.class,
-        V2CompatibilityModeIT.class,
+        SingleTenantCompatibilityModeIT.class,
         CrudCommandsIT.class,
         CacheCommandsIT.class,
         TailCommandsIT.class,
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java 
b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
index c279eeef7..2c8622805 100644
--- a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
@@ -73,7 +73,7 @@ import org.junit.runners.Suite.SuiteClasses;
         GraphQLProfileAliasesIT.class,
         SendEventActionIT.class,
         ScopeIT.class,
-        V2CompatibilityModeIT.class,
+        SingleTenantCompatibilityModeIT.class,
         CrudCommandsIT.class,
         CacheCommandsIT.class,
         TailCommandsIT.class,
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java 
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
similarity index 76%
rename from 
itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
rename to 
itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
index f27089368..8291e60c8 100644
--- a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
+++ 
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
@@ -54,25 +54,27 @@ import java.util.Objects;
 import static org.junit.Assert.*;
 
 /**
- * Integration tests for V2 compatibility mode authentication.
+ * Integration tests for single-tenant compatibility mode authentication.
  * Tests the behavior when switching between V2 and V3 authentication modes
  * using OSGi configuration admin without restarting bundles.
  */
 @RunWith(PaxExam.class)
 @ExamReactorStrategy(PerSuite.class)
-public class V2CompatibilityModeIT extends BaseIT {
+public class SingleTenantCompatibilityModeIT extends BaseIT {
 
-    private final static Logger LOGGER = 
LoggerFactory.getLogger(V2CompatibilityModeIT.class);
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(SingleTenantCompatibilityModeIT.class);
     private final static String CONTEXT_URL = "/cxs/context.json";
     private static final String TEST_SCOPE = "testScope";
     private String TEST_SESSION_ID;
+    /** The tenant the single-tenant compatibility mode runs on, as 
AuthenticationFilter names it. */
+    private static final String COMPATIBILITY_TENANT_ID = "default";
+
     private String TEST_PROFILE_ID;
     private final static String UNOMI_API_KEY_HEADER = "X-Unomi-Api-Key";
     private final static String UNOMI_TENANT_ID_HEADER = "X-Unomi-Tenant-Id";
     private final static String UNOMI_PEER_HEADER = "X-Unomi-Peer";
 
     private boolean originalV2Mode;
-    private String originalDefaultTenantId;
     private V2ThirdPartyConfigService v2ThirdPartyConfigService;
 
     @Before
@@ -82,26 +84,30 @@ public class V2CompatibilityModeIT extends BaseIT {
         v2ThirdPartyConfigService = 
getService(V2ThirdPartyConfigService.class);
 
         TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+
+        // The compatibility mode runs on its own tenant, not the one BaseIT 
works in, so an event it
+        // carries is validated against the scopes of that tenant. Create the 
scope there too, then
+        // put the context back where BaseIT left it.
+        
executionContextManager.setCurrentContext(executionContextManager.createContext(COMPATIBILITY_TENANT_ID));
+        try {
+            TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+        } finally {
+            
executionContextManager.setCurrentContext(executionContextManager.createContext(testTenant.getItemId()));
+        }
         keepTrying("Scope "+ TEST_SCOPE +" not found in the required time", () 
-> scopeService.getScope(TEST_SCOPE),
                 Objects::nonNull, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
 
         // Store original V2 mode setting and default tenant ID
-        originalV2Mode = 
restAuthenticationConfig.isV2CompatibilityModeEnabled();
-        originalDefaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+        originalV2Mode = 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled();
 
-        // Configure V2 compatibility mode to use the BaseIT test tenant as 
default
+        // Configure single-tenant compatibility mode to use the BaseIT test 
tenant as default
         Map<String, Object> v2Config = new HashMap<>();
-        v2Config.put("v2.compatibilitymode.enabled", false); // Start in V3 
mode
-        v2Config.put("v2.compatibilitymode.defaultTenantId", TEST_TENANT_ID); 
// Use BaseIT tenant
+        v2Config.put("singletenantcompatibility.enabled", false); // Start in 
V3 mode
 
         updateConfiguration(null,
                 "org.apache.unomi.rest.authentication",
                 v2Config);
 
-        // Wait for configuration to be applied
-        keepTrying("V2 compatibility configuration not applied in the required 
time",
-                () -> 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId(),
-                tenantId -> TEST_TENANT_ID.equals(tenantId), 
DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
         // Create test profile
         Profile profile = new Profile(TEST_PROFILE_ID);
@@ -118,10 +124,7 @@ public class V2CompatibilityModeIT extends BaseIT {
         try {
             // Restore original V2 mode setting and default tenant ID
             Map<String, Object> originalConfig = new HashMap<>();
-            originalConfig.put("v2.compatibilitymode.enabled", originalV2Mode);
-            if (originalDefaultTenantId != null) {
-                originalConfig.put("v2.compatibilitymode.defaultTenantId", 
originalDefaultTenantId);
-            }
+            originalConfig.put("singletenantcompatibility.enabled", 
originalV2Mode);
 
             updateConfiguration(null,
                     "org.apache.unomi.rest.authentication",
@@ -149,45 +152,45 @@ public class V2CompatibilityModeIT extends BaseIT {
 
     @Test
     public void testV2CompatibilityModeSwitch() throws Exception {
-        LOGGER.info("Starting V2 compatibility mode switch test");
+        LOGGER.info("Starting single-tenant compatibility mode switch test");
 
         // STEP 1: Test V3 mode (default) - V2 requests should be rejected, V3 
requests should work
         LOGGER.info("STEP 1: Testing V3 mode (default)");
         testV3ModeBehavior();
 
-        // STEP 2: Switch to V2 compatibility mode
-        LOGGER.info("STEP 2: Switching to V2 compatibility mode");
+        // STEP 2: Switch to single-tenant compatibility mode
+        LOGGER.info("STEP 2: Switching to single-tenant compatibility mode");
         updateConfiguration(null,
                 "org.apache.unomi.rest.authentication",
-                "v2.compatibilitymode.enabled",
+                "singletenantcompatibility.enabled",
                 true);
 
         // Wait for configuration to take effect
-        keepTrying("V2 compatibility mode not enabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+        keepTrying("single-tenant compatibility mode not enabled in the 
required time",
+                () -> 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
                 enabled -> enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
 
         // STEP 3: Test V2 mode - V2 requests should work, V3 requests should 
be rejected
-        LOGGER.info("STEP 3: Testing V2 compatibility mode");
+        LOGGER.info("STEP 3: Testing single-tenant compatibility mode");
         testV2ModeBehavior();
 
         // STEP 4: Switch back to V3 mode
         LOGGER.info("STEP 4: Switching back to V3 mode");
         updateConfiguration(null,
                 "org.apache.unomi.rest.authentication",
-                "v2.compatibilitymode.enabled",
+                "singletenantcompatibility.enabled",
                 false);
 
         // Wait for configuration to take effect
-        keepTrying("V2 compatibility mode not disabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+        keepTrying("single-tenant compatibility mode not disabled in the 
required time",
+                () -> 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
                 enabled -> !enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
 
         // STEP 5: Test V3 mode again - V2 requests should be rejected, V3 
requests should work
         LOGGER.info("STEP 5: Testing V3 mode again");
         testV3ModeBehavior();
 
-        LOGGER.info("V2 compatibility mode switch test completed 
successfully");
+        LOGGER.info("single-tenant compatibility mode switch test completed 
successfully");
     }
 
     /**
@@ -242,7 +245,7 @@ public class V2CompatibilityModeIT extends BaseIT {
     }
 
     /**
-     * Test behavior in V2 compatibility mode:
+     * Test behavior in single-tenant compatibility mode:
      * - V2 requests (no auth for public endpoints) should work
      * - V3 requests should be rejected
      */
@@ -254,21 +257,26 @@ public class V2CompatibilityModeIT extends BaseIT {
         HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
         request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
         TestUtils.RequestResponse response = 
executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V2-style request should work in V2 compatibility mode", 
200, response.getStatusCode());
+        assertEquals("V2-style request should work in single-tenant 
compatibility mode", 200, response.getStatusCode());
+        // The profile this request created belongs to the tenant the 
compatibility mode runs on, which
+        // is not the tenant BaseIT works in. Read that profile back below, so 
the write and the read
+        // both go through the compatibility path.
+        String compatibilityProfileId = 
response.getContextResponse().getProfileId();
+        assertNotNull("V2-style request should have created a profile", 
compatibilityProfileId);
 
         // Test V2-style request with X-Unomi-Peer header (V2 third-party 
auth) - should work
         request = new HttpPost(getFullUrl(CONTEXT_URL));
         request.addHeader(UNOMI_PEER_HEADER, 
"670c26d1cc413346c3b2fd9ce65dab41");
         request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
         response = executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V2-style request with X-Unomi-Peer should work in V2 
compatibility mode", 200, response.getStatusCode());
+        assertEquals("V2-style request with X-Unomi-Peer should work in 
single-tenant compatibility mode", 200, response.getStatusCode());
 
         // Test V3-style request with public API key - in V2 mode, V3 API keys 
are ignored (request succeeds but no events processed)
         request = new HttpPost(getFullUrl(CONTEXT_URL));
         request.addHeader(UNOMI_API_KEY_HEADER, testPublicKeyValue);
         request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
         response = executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V3-style request with public API key should return 200 
in V2 compatibility mode", 200, response.getStatusCode());
+        assertEquals("V3-style request with public API key should return 200 
in single-tenant compatibility mode", 200, response.getStatusCode());
         assertEquals("V3-style request with public API key should have 0 
processed events in V2 mode", 0, 
response.getContextResponse().getProcessedEvents());
 
         // Test V3-style request with private API key - in V2 mode, V3 API 
keys are ignored (request succeeds but no events processed)
@@ -276,11 +284,11 @@ public class V2CompatibilityModeIT extends BaseIT {
         addPrivateTenantAuth(request, testTenant, testPrivateKeyValue);
         request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
         response = executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V3-style request with private API key should return 200 
in V2 compatibility mode", 200, response.getStatusCode());
+        assertEquals("V3-style request with private API key should return 200 
in single-tenant compatibility mode", 200, response.getStatusCode());
         assertEquals("V3-style request with private API key should have 0 
processed events in V2 mode", 0, 
response.getContextResponse().getProcessedEvents());
 
         // Test private endpoint with JAAS authentication - should work (like 
V2)
-        HttpGet getRequest = new HttpGet(getFullUrl("/cxs/profiles/" + 
TEST_PROFILE_ID));
+        HttpGet getRequest = new HttpGet(getFullUrl("/cxs/profiles/" + 
compatibilityProfileId));
 
         BasicCredentialsProvider credsProvider = new 
BasicCredentialsProvider();
         credsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
@@ -295,26 +303,26 @@ public class V2CompatibilityModeIT extends BaseIT {
                 .setDefaultRequestConfig(requestConfig)
                 .build()) {
             try (CloseableHttpResponse jaasResponse = 
adminClient.execute(getRequest)) {
-                assertEquals("Private endpoint with JAAS auth should work in 
V2 compatibility mode", 200, jaasResponse.getStatusLine().getStatusCode());
+                assertEquals("Private endpoint with JAAS auth should work in 
single-tenant compatibility mode", 200, 
jaasResponse.getStatusLine().getStatusCode());
             }
             try (CloseableHttpResponse privacyResponse = 
adminClient.execute(new HttpGet(getFullUrl("/cxs/privacy/info")))) {
-                assertEquals("GET /cxs/privacy/info with Karaf auth should 
work in V2 compatibility mode", 200, 
privacyResponse.getStatusLine().getStatusCode());
+                assertEquals("GET /cxs/privacy/info with Karaf auth should 
work in single-tenant compatibility mode", 200, 
privacyResponse.getStatusLine().getStatusCode());
             }
         }
     }
 
     @Test
     public void testV2CompatibilityModeWithProtectedEvents() throws Exception {
-        LOGGER.info("Testing V2 compatibility mode with protected events");
+        LOGGER.info("Testing single-tenant compatibility mode with protected 
events");
 
-        // Switch to V2 compatibility mode
+        // Switch to single-tenant compatibility mode
         updateConfiguration(null,
                 "org.apache.unomi.rest.authentication",
-                "v2.compatibilitymode.enabled",
+                "singletenantcompatibility.enabled",
                 true);
 
-        keepTrying("V2 compatibility mode not enabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+        keepTrying("single-tenant compatibility mode not enabled in the 
required time",
+                () -> 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
                 enabled -> enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
 
         // Test protected event (login) without V2 third-party authentication 
- should be rejected
@@ -363,79 +371,13 @@ public class V2CompatibilityModeIT extends BaseIT {
         assertEquals("Non-protected event without auth should have 1 processed 
event", 1, response.getContextResponse().getProcessedEvents());
     }
 
-    @Test
-    public void testV2CompatibilityModeDefaultTenant() throws Exception {
-        LOGGER.info("Testing V2 compatibility mode default tenant behavior");
-
-        // Verify the configuration was applied correctly in setUp()
-        assertEquals("Default tenant should be set to BaseIT tenant", 
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
-        // Switch to V2 compatibility mode
-        updateConfiguration(null,
-                "org.apache.unomi.rest.authentication",
-                "v2.compatibilitymode.enabled",
-                true);
-
-        keepTrying("V2 compatibility mode not enabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
-                enabled -> enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
-
-        // Verify the configuration was applied
-        assertTrue("V2 compatibility mode should be enabled", 
restAuthenticationConfig.isV2CompatibilityModeEnabled());
-        assertEquals("Default tenant should be set to BaseIT tenant", 
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
-        // Test that requests work with the BaseIT tenant as default
-        ContextRequest contextRequest = new ContextRequest();
-        contextRequest.setSessionId(TEST_SESSION_ID);
-
-        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
-        request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
-        TestUtils.RequestResponse response = 
executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V2-style request should work with BaseIT tenant as 
default", 200, response.getStatusCode());
-    }
-
-    @Test
-    public void testV2CompatibilityModeConfigurationPersistence() throws 
Exception {
-        LOGGER.info("Testing V2 compatibility mode configuration persistence");
-
-        // Test that configuration changes persist across service updates
-        updateConfiguration(null,
-                "org.apache.unomi.rest.authentication",
-                "v2.compatibilitymode.enabled",
-                true);
-
-        keepTrying("V2 compatibility mode not enabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
-                enabled -> enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
-
-        // Verify configuration is applied
-        assertTrue("V2 compatibility mode should be enabled", 
restAuthenticationConfig.isV2CompatibilityModeEnabled());
-        assertEquals("Default tenant should persist", TEST_TENANT_ID, 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
-        // Update services to simulate service restart
-        updateServices();
-
-        // Verify configuration persists
-        assertTrue("V2 compatibility mode should persist after service 
update", restAuthenticationConfig.isV2CompatibilityModeEnabled());
-        assertEquals("Default tenant should persist after service update", 
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
-        // Test that behavior is still correct
-        ContextRequest contextRequest = new ContextRequest();
-        contextRequest.setSessionId(TEST_SESSION_ID);
-
-        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
-        request.setEntity(new 
StringEntity(getObjectMapper().writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
-        TestUtils.RequestResponse response = 
executeContextJSONRequest(request, TEST_SESSION_ID);
-        assertEquals("V2-style request should still work after service 
update", 200, response.getStatusCode());
-    }
-
     @Test
     public void testV2CompatibilityProtectedEventNegativeCases() throws 
Exception {
-        LOGGER.info("Testing V2 compatibility mode - protected event negative 
cases");
+        LOGGER.info("Testing single-tenant compatibility mode - protected 
event negative cases");
 
-        updateConfiguration(null, "org.apache.unomi.rest.authentication", 
"v2.compatibilitymode.enabled", true);
-        keepTrying("V2 compatibility mode not enabled in the required time",
-                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+        updateConfiguration(null, "org.apache.unomi.rest.authentication", 
"singletenantcompatibility.enabled", true);
+        keepTrying("single-tenant compatibility mode not enabled in the 
required time",
+                () -> 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
                 enabled -> enabled, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
 
         Event loginEvent = new Event();
diff --git a/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc 
b/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
index 59350e8ec..0b2e6e165 100644
--- a/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
+++ b/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
@@ -350,7 +350,7 @@ Before starting the migration, please ensure that:
 - You did practice the migration in a staging environment, NEVER migrate a 
production environment without prior validation
 - You are currently running Apache Unomi 3.0 (or a later 3.0.x version)
 - You understand the multi-tenancy impact on your data model
-- You have a plan to update client applications to tenant API keys (or 
temporary <<_v2_compatibility_mode,V2 compatibility mode>> only if coming from 
2.x)
+- You have a plan to update client applications to tenant API keys (or 
temporary <<_v2_compatibility_mode,single-tenant compatibility mode>> only if 
coming from 2.x)
 - You have reviewed the <<_client_facing_hardening_3_1,client-facing 
hardening>> notes (cookie-only public profile binding, HttpOnly default, 
required passwords, privileged REST roles)
 - You know how to obtain plaintext API keys after upgrade (regenerate via 
`/cxs/tenants/{id}/apikeys`; create responses expose masked keys only)
 
@@ -376,7 +376,7 @@ Scripts applied for the 3.1 line include (under 
`tools/shell-commands/.../META-I
 * `migrate-3.1.0-05-fixSystemItemIds`
 * `migrate-3.1.0-10-tenantInitialization` (creates the default tenant used for 
isolation)
 * `migrate-3.1.0-15-updateLegacyQueryBuilder`
-4. **Update client applications** to the 3.1 authentication model (public key 
header / tenant private key), or enable <<_v2_compatibility_mode,V2 
compatibility mode>> only when migrating **from Unomi 2.x**.
+4. **Update client applications** to the 3.1 authentication model (public key 
header / tenant private key), or enable <<_v2_compatibility_mode,single-tenant 
compatibility mode>> only when migrating **from Unomi 2.x**.
 5. **Start your Apache Unomi 3.1 cluster**.
 6. **Regenerate and store tenant API keys** (plaintext is returned only from 
the key-creation endpoint), then **test** applications end to end.
 
@@ -388,20 +388,20 @@ Unomi 3.1 does **not** provide a separate "3.0 
compatibility mode" system proper
 
 Adopt tenant public API keys for public endpoints and tenant private keys or 
JAAS for administrative work. See <<_multitenancy,Multi-tenancy>> and the 
authentication examples in this guide.
 
-==== Option 2: V2 compatibility mode (2.x clients only)
+==== Option 2: single-tenant compatibility mode (2.x clients only)
 
-If you are migrating from **Unomi 2.x** (not 3.0), you can enable 
<<_v2_compatibility_mode,V2 compatibility mode>> so legacy clients work without 
API keys during a phased rollout:
+If you are migrating from **Unomi 2.x** (not 3.0), you can enable 
<<_v2_compatibility_mode,single-tenant compatibility mode>> so legacy clients 
work without API keys during a phased rollout:
 
 [source,properties]
 ----
 # etc/org.apache.unomi.rest.authentication.cfg
-v2.compatibilitymode.enabled = true
-v2.compatibilitymode.defaultTenantId = default
+singletenantcompatibility.enabled = true
+v2.compatibilitymode.tenantId = default
 ----
 
-Environment variable equivalent: 
`UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED=true`
+Environment variable equivalent: 
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true`
 
-WARNING: V2 compatibility mode is intended for migration from 2.x only. It 
bypasses tenant API key requirements on public endpoints. Disable it once all 
clients use 3.1 authentication.
+WARNING: single-tenant compatibility mode is intended for migration from 2.x 
only. It bypasses tenant API key requirements on public endpoints. Disable it 
once all clients use 3.1 authentication.
 
 === Post Migration
 
diff --git a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc 
b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
index 623820939..f0468f570 100644
--- a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
+++ b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
@@ -13,17 +13,17 @@
 //
 
 [#_v2_compatibility_mode]
-=== V2 compatibility mode
+=== single-tenant compatibility mode
 
-This document explains how to use the V2 compatibility mode in Apache Unomi 
V3, which allows V2 client applications to work with Unomi V3 without requiring 
API keys.
+This document explains how to use the single-tenant compatibility mode in 
Apache Unomi V3, which allows V2 client applications to work with Unomi V3 
without requiring API keys.
 
 ==== Overview
 
-The V2 compatibility mode is designed to ease the migration from Unomi V2 to 
V3 by allowing V2 clients to continue working without immediate changes to 
their authentication logic. This mode provides backward compatibility while 
still leveraging the multi-tenant architecture of V3.
+The single-tenant compatibility mode is designed to ease the migration from 
Unomi V2 to V3 by allowing V2 clients to continue working without immediate 
changes to their authentication logic. This mode provides backward 
compatibility while still leveraging the multi-tenant architecture of V3.
 
 ===== How It Works
 
-When V2 compatibility mode is enabled:
+When single-tenant compatibility mode is enabled:
 
 - **Public endpoints** (like `/context.json`) require no authentication (like 
V2)
 - **Protected events** (like `login`, `updateProperties`) require IP + 
X-Unomi-Peer (like V2)
@@ -35,7 +35,7 @@ This allows V2 clients to work with Unomi V3 immediately 
after migration, giving
 
 ==== Prerequisites
 
-Before enabling V2 compatibility mode, ensure that:
+Before enabling single-tenant compatibility mode, ensure that:
 
 1. **Data Migration Completed**: Your V2 data has been migrated to a tenant 
using the migration scripts
 2. **Default Tenant Exists**: A default tenant exists that will be used for 
all operations
@@ -45,7 +45,7 @@ Before enabling V2 compatibility mode, ensure that:
 
 ===== Enable V2 Compatibility Mode
 
-You can also set the environment variable 
`UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED=true` (maps to 
`org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled` in 
`custom.system.properties`).
+You can also set the environment variable 
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true` (maps to 
`org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled` in 
`custom.system.properties`).
 
 1. **Edit the configuration file**:
    ```bash
@@ -53,13 +53,13 @@ You can also set the environment variable 
`UNOMI_REST_AUTHENTICATION_V2COMPATIBI
    vi etc/org.apache.unomi.rest.authentication.cfg
    ```
 
-2. **Enable V2 compatibility mode**:
+2. **Enable single-tenant compatibility mode**:
    ```properties
-   # Enable V2 compatibility mode
-   v2.compatibilitymode.enabled = true
+   # Enable single-tenant compatibility mode
+   singletenantcompatibility.enabled = true
    
    # Set the default tenant ID (should match the tenant ID used during 
migration)
-   v2.compatibilitymode.defaultTenantId = your-migration-tenant-id
+   v2.compatibilitymode.tenantId = your-migration-tenant-id
    ```
 
 3. **Restart the server** to apply the configuration changes:
@@ -73,7 +73,7 @@ You can also set the environment variable 
`UNOMI_REST_AUTHENTICATION_V2COMPATIBI
 
 ===== Configuration Management
 
-V2 compatibility mode is managed through configuration files only. This 
approach is safer and prevents accidental changes to authentication settings.
+single-tenant compatibility mode is managed through configuration files only. 
This approach is safer and prevents accidental changes to authentication 
settings.
 
 ==== Migration Workflow
 
@@ -86,18 +86,18 @@ First, migrate your data with Apache Unomi stopped using 
the single shell comman
 unomi:migrate 2.0.0
 ```
 
-The `migrate-3.1.0-10-tenantInitialization` script (run as part of that chain 
when migrating through 3.1) creates a default tenant used for V2 compatibility 
mode. See <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>> and 
<<_shell_commands,Shell commands>>.
+The `migrate-3.1.0-10-tenantInitialization` script (run as part of that chain 
when migrating through 3.1) creates a default tenant used for single-tenant 
compatibility mode. See <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>> 
and <<_shell_commands,Shell commands>>.
 
 ===== Step 2: Enable V2 Compatibility Mode
 
-Enable V2 compatibility mode by updating the configuration file:
+Enable single-tenant compatibility mode by updating the configuration file:
 
 ```bash
 # Edit the configuration file
 vi etc/org.apache.unomi.rest.authentication.cfg
 
-# Set v2.compatibilitymode.enabled = true
-# Set v2.compatibilitymode.defaultTenantId = your-tenant-id
+# Set singletenantcompatibility.enabled = true
+# Set v2.compatibilitymode.tenantId = your-tenant-id
 
 # Restart the server to apply changes
 ./bin/stop
@@ -126,8 +126,8 @@ RestAssured.given()
 Over time, gradually update your clients to use V3 authentication:
 
 1. **Update client applications** to use API keys
-2. **Test with V3 authentication** while keeping V2 compatibility mode enabled
-3. **Disable V2 compatibility mode** once all clients are updated
+2. **Test with V3 authentication** while keeping single-tenant compatibility 
mode enabled
+3. **Disable single-tenant compatibility mode** once all clients are updated
 
 ==== Client Migration Examples
 
@@ -135,7 +135,7 @@ Over time, gradually update your clients to use V3 
authentication:
 
 **V2 Client (continues to work)**:
 ```java
-// This continues to work in V2 compatibility mode
+// This continues to work in single-tenant compatibility mode
 RestAssured.authentication = RestAssured.preemptive()
     .basic("karaf", System.getenv("UNOMI_ROOT_PASSWORD"));
 
@@ -158,16 +158,16 @@ given()
 
 ===== Gradual Migration Strategy
 
-1. **Phase 1**: Enable V2 compatibility mode, V2 clients continue working
+1. **Phase 1**: Enable single-tenant compatibility mode, V2 clients continue 
working
 2. **Phase 2**: Develop and test V3 clients alongside V2 clients
 3. **Phase 3**: Migrate clients one by one to V3 authentication
-4. **Phase 4**: Disable V2 compatibility mode once all clients are migrated
+4. **Phase 4**: Disable single-tenant compatibility mode once all clients are 
migrated
 
 ==== Security Considerations
 
 ===== V2 Compatibility Mode Security
 
-When V2 compatibility mode is enabled:
+When single-tenant compatibility mode is enabled:
 
 - **Public endpoints** are accessible without authentication (same as V2)
 - **Protected events** require IP + X-Unomi-Peer authentication (same as V2)
@@ -177,7 +177,7 @@ When V2 compatibility mode is enabled:
 
 ===== Protected Events in V2 Compatibility Mode
 
-In V2 compatibility mode, protected event types are configured dynamically 
using the V2 third-party configuration file. By default, the following event 
types are protected:
+In single-tenant compatibility mode, protected event types are configured 
dynamically using the V2 third-party configuration file. By default, the 
following event types are protected:
 
 - `login` - User authentication events
 - `updateProperties` - Profile property updates
@@ -244,10 +244,10 @@ The V2 third-party configuration supports dynamic updates:
 
 ===== Recommendations
 
-1. **Use V2 compatibility mode temporarily** during migration
+1. **Use single-tenant compatibility mode temporarily** during migration
 2. **Plan for gradual migration** to V3 authentication
 3. **Monitor access patterns** during the transition
-4. **Disable V2 compatibility mode** once migration is complete
+4. **Disable single-tenant compatibility mode** once migration is complete
 
 ==== Troubleshooting
 
@@ -255,8 +255,8 @@ The V2 third-party configuration supports dynamic updates:
 
 **V2 clients still not working**:
 - Check configuration file: `etc/org.apache.unomi.rest.authentication.cfg`
-- Verify `v2.compatibilitymode.enabled = true`
-- Ensure `v2.compatibilitymode.defaultTenantId` matches the tenant ID used 
during migration
+- Verify `singletenantcompatibility.enabled = true`
+- Ensure `v2.compatibilitymode.tenantId` matches the tenant ID used during 
migration
 - Ensure the tenant exists and is accessible
 
 **Authentication errors**:
@@ -291,7 +291,7 @@ Once all clients are migrated to V3 authentication:
 
 1. **Update configuration**:
    ```properties
-   v2.compatibilitymode.enabled = false
+   singletenantcompatibility.enabled = false
    ```
 
 2. **Restart the server**:
@@ -306,14 +306,14 @@ Once all clients are migrated to V3 authentication:
 
 ==== Testing V2 Compatibility Mode
 
-The existing test framework supports testing V2 compatibility mode using 
system properties.
+The existing test framework supports testing single-tenant compatibility mode 
using system properties.
 
 ===== Running Tests in V2 Compatibility Mode
 
-To run tests with V2 compatibility mode enabled:
+To run tests with single-tenant compatibility mode enabled:
 
 ```bash
-# Enable V2 compatibility mode for tests
+# Enable single-tenant compatibility mode for tests
 mvn test -Dunomi.v2.compatibility.mode=true
 
 # Or set the property in your test environment
@@ -323,17 +323,17 @@ mvn test
 
 ===== Test Framework Integration
 
-The test framework automatically detects V2 compatibility mode and uses the 
appropriate client:
+The test framework automatically detects single-tenant compatibility mode and 
uses the appropriate client:
 
 - **V2 Compatibility Mode Enabled**: Uses `UnomiV2Client` for all tests
 - **V2 Compatibility Mode Disabled**: Uses normal V2/V3 detection logic
 
-This allows you to test both V2 compatibility mode and normal V3 mode using 
the same test suite.
+This allows you to test both single-tenant compatibility mode and normal V3 
mode using the same test suite.
 
 ===== Example Test Execution
 
 ```bash
-# Test with V2 compatibility mode (server should be configured for V2 
compatibility)
+# Test with single-tenant compatibility mode (server should be configured for 
V2 compatibility)
 mvn test -Dunomi.v2.compatibility.mode=true -Dunomi.url=http://localhost:8181
 
 # Test with normal V3 mode
@@ -342,7 +342,7 @@ mvn test -Dunomi.url=http://localhost:8181
 
 ==== Conclusion
 
-The V2 compatibility mode provides a smooth migration path from Unomi V2 to 
V3, allowing you to:
+The single-tenant compatibility mode provides a smooth migration path from 
Unomi V2 to V3, allowing you to:
 
 - **Maintain existing V2 clients** during migration
 - **Gradually migrate** to V3 authentication
diff --git a/manual/src/main/asciidoc/whats-new.adoc 
b/manual/src/main/asciidoc/whats-new.adoc
index d49febed1..2433bcef3 100644
--- a/manual/src/main/asciidoc/whats-new.adoc
+++ b/manual/src/main/asciidoc/whats-new.adoc
@@ -22,7 +22,7 @@ Complete tenant isolation for profiles, events, segments, 
rules, and schemas. Pu
 
 * Operator guide: <<_multitenancy,Multi-tenancy>>
 * Migration: <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>> (includes 
<<_client_facing_hardening_3_1,client-facing hardening>>)
-* Migrating from Unomi 2.x: <<_v2_compatibility_mode,V2 compatibility mode>> 
(`v2.compatibilitymode.enabled` in `org.apache.unomi.rest.authentication.cfg`)
+* Migrating from Unomi 2.x: <<_v2_compatibility_mode,single-tenant 
compatibility mode>> (`singletenantcompatibility.enabled` in 
`org.apache.unomi.rest.authentication.cfg`)
 
 ==== Security hardening (credentials, profile binding, privileged APIs)
 
diff --git a/package/src/main/resources/etc/custom.system.properties 
b/package/src/main/resources/etc/custom.system.properties
index 4dd435a78..9cb7b8caf 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -524,5 +524,4 @@ 
org.apache.unomi.campaigns.refresh.interval=${env:UNOMI_CAMPAIGNS_REFRESH_INTERV
 
#######################################################################################################################
 ## REST API Authorization Settings                                             
                                      ##
 
#######################################################################################################################
-org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled=${env:UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED:-false}
-org.apache.unomi.rest.authentication.v2CompatibilityDefaultTenantId=${env:UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYDEFAULTTENANTID:-default}
+org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled=${env:UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED:-false}
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
index 0e990ffaa..6f547f935 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
@@ -41,7 +41,6 @@ import javax.ws.rs.container.ContainerRequestFilter;
 import javax.ws.rs.container.PreMatching;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
-import org.apache.commons.lang3.StringUtils;
 
 import java.io.IOException;
 import java.util.Base64;
@@ -60,6 +59,13 @@ import java.util.Set;
 @Priority(Priorities.AUTHENTICATION)
 public class AuthenticationFilter implements ContainerRequestFilter {
 
+    /**
+     * The tenant that single-tenant compatibility mode runs on. A client from 
before Unomi 3.1 knows
+     * no tenant, so it never names one, and nothing outside this class reads 
this value. It is a
+     * constant rather than a setting for that reason.
+     */
+    private static final String COMPATIBILITY_TENANT_ID = "default";
+
     private static final String UNOMI_API_KEY_HEADER = "X-Unomi-Api-Key";
     private static final String UNOMI_TENANT_ID_HEADER = "X-Unomi-Tenant-Id";
     private static final Logger logger = 
LoggerFactory.getLogger(AuthenticationFilter.class);
@@ -135,9 +141,9 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
         try {
             String path = requestContext.getUriInfo().getPath();
 
-            // Check if V2 compatibility mode is enabled
-            if (restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
-                handleV2CompatibilityMode(requestContext, path);
+            // Check if single-tenant compatibility mode is enabled
+            if 
(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()) {
+                handleSingleTenantCompatibilityMode(requestContext, path);
                 return;
             }
 
@@ -280,42 +286,39 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
     }
 
     /**
-     * Handle authentication in V2 compatibility mode.
+     * Handle authentication in single-tenant compatibility mode.
      * In this mode:
      * - Public endpoints (like /context.json) require no authentication (like 
V2)
      * - Protected events require IP + X-Unomi-Peer (like V2)
      * - Private endpoints require system administrator authentication (like 
V2)
      * - A default tenant is automatically used for all operations
      */
-    private void handleV2CompatibilityMode(ContainerRequestContext 
requestContext, String path) throws IOException {
+    private void handleSingleTenantCompatibilityMode(ContainerRequestContext 
requestContext, String path) throws IOException {
         // For public paths, allow access without authentication (like V2)
         if (isPublicPath(requestContext)) {
-            String defaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
-            if (StringUtils.isNotBlank(defaultTenantId)) {
-                Tenant defaultTenant = 
tenantService.getTenant(defaultTenantId);
-                if (defaultTenant == null) {
-                    logger.error("V2 compatibility mode: configured default 
tenant '{}' does not exist", defaultTenantId);
-                    unauthorized(requestContext);
-                    return;
-                }
-                // Create a guest subject for public endpoints
-                Subject subject = 
securityService.createSubject(defaultTenantId, false);
-
-                // Set CXF security context
-                JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
-                    new RolePrefixSecurityContextImpl(subject, 
ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
-
-                // Set the security service subject
-                securityService.setCurrentSubject(subject);
-
-                // Set the execution context for the default tenant
-                
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
-                return;
-            } else {
-                logger.warn("V2 compatibility mode: public path request denied 
because v2CompatibilityDefaultTenantId is not configured");
+            String tenantId = COMPATIBILITY_TENANT_ID;
+            // V2 knew no tenant, so a V2 client cannot name one and cannot 
create one. Create the
+            // tenant this mode runs on, rather than refuse every request 
until an operator creates
+            // it by hand.
+            try {
+                tenantService.getOrCreateTenant(tenantId, 
Collections.singletonMap("name", tenantId));
+            } catch (RuntimeException e) {
+                logger.error("single-tenant compatibility mode: could not 
obtain tenant '{}'", tenantId, e);
                 unauthorized(requestContext);
                 return;
             }
+            // Create a guest subject for public endpoints
+            Subject subject = securityService.createSubject(tenantId, false);
+
+            // Set CXF security context
+            JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
+                new RolePrefixSecurityContextImpl(subject, ROLE_CLASSIFIER, 
ROLE_CLASSIFIER_TYPE));
+
+            // Set the security service subject
+            securityService.setCurrentSubject(subject);
+
+            
executionContextManager.setCurrentContext(executionContextManager.createContext(tenantId));
+            return;
         }
 
         // For private endpoints, require system administrator authentication 
(like V2)
@@ -330,47 +333,43 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
                 // A null security context here means auth was rejected or an 
unexpected state occurred — deny either way.
                 SecurityContext securityContext = 
JAXRSUtils.getCurrentMessage().get(SecurityContext.class);
                 if (securityContext == null) {
-                    logger.debug("V2 compatibility mode: no security context 
after JAAS filter, denying access");
+                    logger.debug("single-tenant compatibility mode: no 
security context after JAAS filter, denying access");
                     unauthorized(requestContext);
                     return;
                 }
                 Subject jaasSubject = ((RolePrefixSecurityContextImpl) 
securityContext).getSubject();
 
-                // Private endpoints in V2 compatibility mode require system 
administrator
+                // Private endpoints in single-tenant compatibility mode 
require system administrator
                 // authentication (like V2) — a JAAS login alone isn't enough, 
since any Karaf
                 // user (not just admins) can authenticate against the realm.
                 if 
(!securityService.extractRolesFromSubject(jaasSubject).contains(UnomiRoles.ADMINISTRATOR))
 {
-                    logger.debug("V2 compatibility mode: authenticated user 
lacks administrator role, denying access to private endpoint");
+                    logger.debug("single-tenant compatibility mode: 
authenticated user lacks administrator role, denying access to private 
endpoint");
                     unauthorized(requestContext);
                     return;
                 }
 
-                // Build a merged subject that combines the JAAS principals 
with tenant admin
-                // principals for the default tenant, so that 
resolveTenantId() can find it downstream.
-                String defaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+                // Build a merged subject that combines the JAAS principals 
with the tenant admin
+                // principals, so that resolveTenantId() can find the tenant 
downstream.
+                String tenantId = COMPATIBILITY_TENANT_ID;
                 Subject mergedSubject = new Subject();
                 
mergedSubject.getPrincipals().addAll(jaasSubject.getPrincipals());
-                if (StringUtils.isNotBlank(defaultTenantId)) {
-                    
mergedSubject.getPrincipals().addAll(securityService.createSubject(defaultTenantId,
 true).getPrincipals());
-                    Set<String> roles = 
securityService.extractRolesFromSubject(mergedSubject);
-                    Set<String> permissions = new HashSet<>();
-                    for (String role : roles) {
-                        
permissions.addAll(securityService.getPermissionsForRole(role));
-                    }
-                    executionContextManager.setCurrentContext(new 
ExecutionContext(defaultTenantId, roles, permissions));
-                } else {
-                    
executionContextManager.setCurrentContext(ExecutionContext.systemContext());
+                
mergedSubject.getPrincipals().addAll(securityService.createSubject(tenantId, 
true).getPrincipals());
+                Set<String> roles = 
securityService.extractRolesFromSubject(mergedSubject);
+                Set<String> permissions = new HashSet<>();
+                for (String role : roles) {
+                    
permissions.addAll(securityService.getPermissionsForRole(role));
                 }
+                executionContextManager.setCurrentContext(new 
ExecutionContext(tenantId, roles, permissions));
                 JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
                     new RolePrefixSecurityContextImpl(mergedSubject, 
ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
                 securityService.setCurrentSubject(mergedSubject);
                 return;
             } catch (Exception e) {
                 // Only fires for unexpected exceptions — credential failures 
are handled inside JAASAuthenticationFilter.
-                logger.debug("V2 compatibility mode: unexpected exception 
during JAAS processing", e);
+                logger.debug("single-tenant compatibility mode: unexpected 
exception during JAAS processing", e);
             }
         } else {
-            logger.debug("V2 compatibility mode: Missing Basic Auth header for 
private endpoint");
+            logger.debug("single-tenant compatibility mode: Missing Basic Auth 
header for private endpoint");
         }
 
         // If we get here, no valid authentication was provided
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
index ed7022dc0..fe3eed098 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
@@ -61,21 +61,14 @@ public interface RestAuthenticationConfig {
     String getGlobalRoles();
 
     /**
-     * Check if V2 compatibility mode is enabled.
+     * Check if single-tenant compatibility mode is enabled.
      * When enabled, V2 clients can use Unomi V3 without requiring API keys:
      * - Public endpoints (like /context.json) require no authentication (like 
V2)
      * - Private endpoints require system administrator authentication (like 
V2)
      * - A default tenant is automatically used for all operations
      *
-     * @return true if V2 compatibility mode is enabled, false otherwise
+     * @return true if single-tenant compatibility mode is enabled, false 
otherwise
      */
-    boolean isV2CompatibilityModeEnabled();
+    boolean isSingleTenantCompatibilityModeEnabled();
 
-    /**
-     * Get the default tenant ID to use in V2 compatibility mode.
-     * This tenant will be used for all operations when V2 compatibility mode 
is enabled.
-     *
-     * @return the default tenant ID
-     */
-    String getV2CompatibilityDefaultTenantId();
 }
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
index 2d3bf5fa1..a9529e1c7 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
@@ -33,7 +33,7 @@ import java.util.regex.Pattern;
 
 /**
  * Default OSGi-backed implementation of {@link RestAuthenticationConfig} for 
REST endpoint
- * authentication, role mappings, and V2 compatibility mode settings.
+ * authentication, role mappings, and single-tenant compatibility mode 
settings.
  */
 @Component(service = { RestAuthenticationConfig.class}, configurationPid = 
"org.apache.unomi.rest.authentication", immediate = true)
 @Designate(ocd = DefaultRestAuthenticationConfig.Config.class)
@@ -76,8 +76,7 @@ public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig
         ROLES_MAPPING = Collections.unmodifiableMap(roles);
     }
 
-    private volatile boolean v2CompatibilityModeEnabled = false;
-    private volatile String v2CompatibilityDefaultTenantId = "default";
+    private volatile boolean singleTenantCompatibilityModeEnabled = false;
 
     /**
      * Updates authentication settings from OSGi configuration.
@@ -91,19 +90,9 @@ public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig
             LOGGER.warn("Config is null in modified method");
             return;
         }
-        boolean v2Mode = config.v2_compatibilitymode_enabled();
-        String defaultTenant = config.v2_compatibilitymode_defaultTenantId();
-        if (defaultTenant != null) {
-            defaultTenant = defaultTenant.trim();
-        }
-        if (StringUtils.isBlank(defaultTenant)) {
-            LOGGER.warn("v2CompatibilityDefaultTenantId is blank, falling back 
to 'default'");
-            defaultTenant = "default";
-        }
-        LOGGER.info("Configuration updated - v2CompatibilityModeEnabled: {}, 
v2CompatibilityDefaultTenantId: {}",
-                    v2Mode, defaultTenant);
-        this.v2CompatibilityModeEnabled = v2Mode;
-        this.v2CompatibilityDefaultTenantId = defaultTenant;
+        boolean singleTenantMode = config.singletenantcompatibility_enabled();
+        LOGGER.info("Configuration updated - 
singleTenantCompatibilityModeEnabled: {}", singleTenantMode);
+        this.singleTenantCompatibilityModeEnabled = singleTenantMode;
     }
 
 
@@ -123,44 +112,29 @@ public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig
     }
 
     @Override
-    public boolean isV2CompatibilityModeEnabled() {
-        return v2CompatibilityModeEnabled;
+    public boolean isSingleTenantCompatibilityModeEnabled() {
+        return singleTenantCompatibilityModeEnabled;
     }
 
-    @Override
-    public String getV2CompatibilityDefaultTenantId() {
-        return v2CompatibilityDefaultTenantId;
-    }
 
     /**
      * OSGi configuration for REST authentication.
      */
     @ObjectClassDefinition(
         name = "Unomi REST Authentication Configuration",
-        description = "Configuration for Unomi REST authentication including 
V2 compatibility mode"
+        description = "Configuration for Unomi REST authentication including 
single-tenant compatibility mode"
     )
     public @interface Config {
 
         /**
-         * Whether V2 compatibility mode is enabled.
-         *
-         * @return {@code true} when V2 compatibility mode is enabled
-         */
-        @AttributeDefinition(
-            name = "V2 Compatibility Mode Enabled",
-            description = "Enable V2 compatibility mode to allow V2 clients to 
use Unomi V3 without API keys"
-        )
-        boolean v2_compatibilitymode_enabled() default false;
-
-        /**
-         * Default tenant identifier used in V2 compatibility mode.
+         * Whether single-tenant compatibility mode is enabled.
          *
-         * @return the default tenant identifier
+         * @return {@code true} when single-tenant compatibility mode is 
enabled
          */
         @AttributeDefinition(
-            name = "V2 Compatibility Default Tenant ID",
-            description = "Default tenant ID to use in V2 compatibility mode"
+            name = "Single-tenant compatibility mode enabled",
+            description = "Serve clients from before Unomi 3.1, which send no 
tenant API key, and run them all on one tenant"
         )
-        String v2_compatibilitymode_defaultTenantId() default "default";
+        boolean singletenantcompatibility_enabled() default false;
     }
 }
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
 
b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
index 7ed3a0987..9bee93127 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
@@ -388,10 +388,10 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
                     Event eventToSend = new Event(event.getEventType(), 
eventsRequestContext.getSession(), eventsRequestContext.getProfile(), 
event.getScope(),
                             event.getSource(), event.getTarget(), 
event.getProperties(), eventsRequestContext.getTimestamp(), 
event.isPersistent());
                     
eventToSend.setFlattenedProperties(event.getFlattenedProperties());
-                    // Check if V2 compatibility mode is enabled and handle 
V2-style event authorization
-                    if 
(restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
+                    // Check if single-tenant compatibility mode is enabled 
and handle V2-style event authorization
+                    if 
(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()) {
                         if (!isEventAllowedInV2CompatibilityMode(event, 
eventsRequestContext.getRequest())) {
-                            LOGGER.debug("Event {} not authorized in V2 
compatibility mode from IP {}", event.getEventType(), 
eventsRequestContext.getRequest().getRemoteAddr());
+                            LOGGER.debug("Event {} not authorized in 
single-tenant compatibility mode from IP {}", event.getEventType(), 
eventsRequestContext.getRequest().getRemoteAddr());
                             //Don't count the event that failed
                             
eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() 
- 1);
                             continue;
@@ -540,7 +540,7 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
     }
 
     /**
-     * Check if an event is allowed in V2 compatibility mode.
+     * Check if an event is allowed in single-tenant compatibility mode.
      * In V2, protected events required IP + X-Unomi-Peer (third-party key) 
authentication.
      *
      * @param event the event to check
@@ -550,7 +550,7 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
     private boolean isEventAllowedInV2CompatibilityMode(Event event, 
HttpServletRequest request) {
         // Check if this is a protected event type using the V2 third-party 
configuration
         if 
(!v2ThirdPartyConfigService.isProtectedEventType(event.getEventType())) {
-            // Non-protected events are always allowed in V2 compatibility mode
+            // Non-protected events are always allowed in single-tenant 
compatibility mode
             return true;
         }
 
@@ -559,18 +559,18 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
         String thirdPartyKey = request.getHeader("X-Unomi-Peer");
 
         if (StringUtils.isBlank(thirdPartyKey)) {
-            LOGGER.debug("V2 compatibility mode: Protected event {} rejected - 
missing X-Unomi-Peer header", event.getEventType());
+            LOGGER.debug("single-tenant compatibility mode: Protected event {} 
rejected - missing X-Unomi-Peer header", event.getEventType());
             return false;
         }
 
         // Validate the third-party provider using the V2 configuration
         if (!v2ThirdPartyConfigService.validateProviderByKey(thirdPartyKey, 
event.getEventType(), sourceIP)) {
-            LOGGER.debug("V2 compatibility mode: Protected event {} rejected - 
invalid third-party provider key: {} from IP: {}",
+            LOGGER.debug("single-tenant compatibility mode: Protected event {} 
rejected - invalid third-party provider key: {} from IP: {}",
                         event.getEventType(), 
SecurityUtils.maskSecret(thirdPartyKey), sourceIP);
             return false;
         }
 
-        LOGGER.debug("V2 compatibility mode: Protected event {} allowed for 
provider key: {} from IP: {}",
+        LOGGER.debug("single-tenant compatibility mode: Protected event {} 
allowed for provider key: {} from IP: {}",
                     event.getEventType(), 
SecurityUtils.maskSecret(thirdPartyKey), sourceIP);
         return true;
     }
diff --git a/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg 
b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
index db79d2627..58a9475f4 100644
--- a/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
+++ b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
@@ -22,10 +22,9 @@
 # - Public endpoints (like /context.json) require no authentication (like V2)
 # - Private endpoints require system administrator authentication (like V2)
 # - A default tenant is automatically used for all operations
-v2.compatibilitymode.enabled = 
${org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled:-false}
+singletenantcompatibility.enabled = 
${org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled:-false}
 
 # V2 Compatibility Default Tenant ID
-# Default tenant ID to use in V2 compatibility mode
-# This tenant will be used for all operations when V2 compatibility mode is 
enabled
+# Default tenant ID to use in single-tenant compatibility mode
+# This tenant will be used for all operations when single-tenant compatibility 
mode is enabled
 # Should match the tenant ID used during migration (e.g., "default" or 
"system")
-v2.compatibilitymode.defaultTenantId = 
${org.apache.unomi.rest.authentication.v2CompatibilityDefaultTenantId:-default}
diff --git 
a/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
 
b/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
index 016a45756..1b4baac8e 100644
--- 
a/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
+++ 
b/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
@@ -171,15 +171,15 @@ class AuthenticationFilterBlankPasswordTest {
     }
 
     /**
-     * V2 compatibility mode routes every request through {@link 
AuthenticationFilter}'s own
+     * single-tenant compatibility mode routes every request through {@link 
AuthenticationFilter}'s own
      * private-endpoint branch, which consumes the Basic credential at a 
third, separate call site.
      * Without this test that call site is unreachable from the suite: the 
other tests leave
-     * {@code isV2CompatibilityModeEnabled()} at the unstubbed Mockito {@code 
false}, so deleting
+     * {@code isSingleTenantCompatibilityModeEnabled()} at the unstubbed 
Mockito {@code false}, so deleting
      * the guard there would leave every test green.
      */
     @Test
     void filterRejectsBlankPasswordOnAPrivatePathInV2CompatibilityMode() 
throws IOException {
-        
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+        
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
         
when(restAuthenticationConfig.getPublicPathPatterns()).thenReturn(Collections.emptyList());
         ContainerRequestContext requestContext = request("profiles", 
basic("karaf:"));
 
@@ -191,7 +191,7 @@ class AuthenticationFilterBlankPasswordTest {
     /** Control for the V2 branch: a non-blank credential must still reach 
JAAS there too. */
     @Test
     void 
filterPassesNonBlankPasswordToJaasOnAPrivatePathInV2CompatibilityMode() throws 
IOException {
-        
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+        
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
         
when(restAuthenticationConfig.getPublicPathPatterns()).thenReturn(Collections.emptyList());
         ContainerRequestContext requestContext = request("profiles", 
basic("karaf:a-strong-password"));
 
@@ -201,20 +201,19 @@ class AuthenticationFilterBlankPasswordTest {
     }
 
     /**
-     * A public path in V2 compatibility mode authenticates by default tenant, 
ignoring
+     * A public path in single-tenant compatibility mode authenticates by 
default tenant, ignoring
      * {@code Authorization} entirely — so a stray blank Basic header must not 
turn it into a 401.
      */
     @Test
-    void 
filterDoesNotRejectAStrayBlankBasicHeaderOnAPublicPathInV2CompatibilityMode() 
throws IOException {
-        
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+    void 
filterDoesNotRejectAStrayBlankBasicHeaderOnAPublicPathInSingleTenantCompatibilityMode()
 throws IOException {
+        
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
         when(restAuthenticationConfig.getPublicPathPatterns())
                 .thenReturn(Collections.singletonList(Pattern.compile("POST 
context\\.json")));
-        
when(restAuthenticationConfig.getV2CompatibilityDefaultTenantId()).thenReturn("default");
         ContainerRequestContext requestContext = request("context.json", 
basic("someone:"));
 
         filter.filter(requestContext);
 
-        verify(tenantService).getTenant("default");
+        verify(tenantService).getOrCreateTenant(eq("default"), any());
     }
 
     private void assertUnauthorizedWithoutReachingJaas(ContainerRequestContext 
requestContext) throws IOException {
diff --git 
a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
 
b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
index 0359d9dbf..c2c6e474d 100644
--- 
a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
+++ 
b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
@@ -113,6 +113,25 @@ public class TenantServiceImpl implements TenantService {
         }
     }
 
+    @Override
+    public synchronized Tenant getOrCreateTenant(String tenantId, Map<String, 
Object> properties) {
+        Tenant tenant = getTenant(tenantId);
+        if (tenant != null) {
+            return tenant;
+        }
+        try {
+            return createTenant(tenantId, properties);
+        } catch (IllegalArgumentException e) {
+            // Another node created the tenant between the read and the write, 
which is the outcome
+            // this method is asked for. Re-read rather than fail.
+            Tenant concurrent = getTenant(tenantId);
+            if (concurrent == null) {
+                throw e;
+            }
+            return concurrent;
+        }
+    }
+
     @Override
     public Tenant createTenant(String requestedId, Map<String, Object> 
properties) {
         validateTenantId(requestedId);
diff --git 
a/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java 
b/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
index 59723a95f..b33839b28 100644
--- 
a/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
+++ 
b/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
@@ -105,6 +105,12 @@ public class TestTenantService implements TenantService {
                 && secretHashService.verify(plainTextKey, apiKey.getKeyHash());
     }
 
+    @Override
+    public Tenant getOrCreateTenant(String tenantId, Map<String, Object> 
properties) {
+        Tenant tenant = getTenant(tenantId);
+        return tenant != null ? tenant : createTenant(tenantId, properties);
+    }
+
     @Override
     public Tenant createTenant(String tenantId, Map<String, Object> 
properties) {
         Tenant tenant = new Tenant();

Reply via email to