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

sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 20f004bca UNOMI-1009 UNOMI-1010 UNOMI-1012: Document 4.0 breaking 
changes and compatibility mode (#941)
20f004bca is described below

commit 20f004bca2a68bf16be697794e81f524a55d355a
Author: Serge Huber <[email protected]>
AuthorDate: Mon Nov 2 14:19:30 2026 +0100

    UNOMI-1009 UNOMI-1010 UNOMI-1012: Document 4.0 breaking changes and 
compatibility mode (#941)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../itests/SingleTenantCompatibilityModeIT.java    |   6 +-
 .../src/main/asciidoc/building-and-deploying.adoc  |   2 +-
 manual/src/main/asciidoc/builtin-event-types.adoc  |   5 +-
 manual/src/main/asciidoc/condition-validation.adoc |   2 +-
 manual/src/main/asciidoc/configuration.adoc        |  49 +++++--
 .../asciidoc/migrations/migrate-3.0-to-4.0.adoc    |  56 ++++++--
 .../src/main/asciidoc/migrations/migrations.adoc   |   7 +-
 .../asciidoc/migrations/v2-compatibility-mode.adoc | 141 +++++++++------------
 .../asciidoc/migrations/v2-v3-compatibility.adoc   |  60 ++++-----
 manual/src/main/asciidoc/privacy.adoc              |   4 +-
 manual/src/main/asciidoc/recipes.adoc              |   5 +-
 manual/src/main/asciidoc/security.adoc             |   8 +-
 manual/src/main/asciidoc/shell-commands.adoc       | 104 ++++-----------
 manual/src/main/asciidoc/tutorial.adoc             |  15 ++-
 manual/src/main/asciidoc/whats-new.adoc            |   2 +-
 .../rest/authentication/AuthenticationFilter.java  |  20 +--
 .../authentication/RestAuthenticationConfig.java   |   8 +-
 .../authentication/V2ThirdPartyConfigService.java  |  10 +-
 .../rest/service/impl/RestServiceUtilsImpl.java    |   8 +-
 .../org.apache.unomi.rest.authentication.cfg       |  10 +-
 .../AuthenticationFilterBlankPasswordTest.java     |   8 +-
 .../migrate-4.0.0-10-tenantInitialization.groovy   |  10 +-
 22 files changed, 264 insertions(+), 276 deletions(-)

diff --git 
a/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
 
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
index fec79f77f..3450adc4b 100644
--- 
a/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
+++ 
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
@@ -55,7 +55,7 @@ import static org.junit.Assert.*;
 
 /**
  * Integration tests for single-tenant compatibility mode authentication.
- * Tests the behavior when switching between V2 and V3 authentication modes
+ * Tests the behavior when switching between single-tenant compatibility mode 
and normal multi-tenant mode
  * using OSGi configuration admin without restarting bundles.
  */
 @RunWith(PaxExam.class)
@@ -97,12 +97,12 @@ public class SingleTenantCompatibilityModeIT extends BaseIT 
{
         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
+        // Store original compatibility mode setting and default tenant ID
         originalV2Mode = 
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled();
 
         // Configure single-tenant compatibility mode to use the BaseIT test 
tenant as default
         Map<String, Object> v2Config = new HashMap<>();
-        v2Config.put("singletenantcompatibility.enabled", false); // Start in 
V3 mode
+        v2Config.put("singletenantcompatibility.enabled", false); // Start in 
normal multi-tenant mode
 
         updateConfiguration(null,
                 "org.apache.unomi.rest.authentication",
diff --git a/manual/src/main/asciidoc/building-and-deploying.adoc 
b/manual/src/main/asciidoc/building-and-deploying.adoc
index 470b58dc5..be6d99452 100644
--- a/manual/src/main/asciidoc/building-and-deploying.adoc
+++ b/manual/src/main/asciidoc/building-and-deploying.adoc
@@ -574,6 +574,6 @@ A default test page is provided at the following URL:
 ----
 
 This test page loads `/cxs/context.js`, which retrieves or creates a user 
context.
-On Unomi 4.0, public context endpoints require a tenant public API key 
(`X-Unomi-Api-Key`) unless <<_v2_compatibility_mode,V2 compatibility mode>> is 
enabled.
+On Unomi 4.0, public context endpoints require a tenant public API key 
(`X-Unomi-Api-Key`) unless <<_v2_compatibility_mode,single-tenant compatibility 
mode>> is enabled.
 Create a tenant and regenerate keys first (see 
<<_five_minutes_quickstart,5-minute quickstart>> or 
<<_multitenancy,Multi-tenancy>>).
 The page also contains an experimental Facebook Login integration that does 
not yet save the context back to the server.
diff --git a/manual/src/main/asciidoc/builtin-event-types.adoc 
b/manual/src/main/asciidoc/builtin-event-types.adoc
index 00d59b4aa..de6402f90 100644
--- a/manual/src/main/asciidoc/builtin-event-types.adoc
+++ b/manual/src/main/asciidoc/builtin-event-types.adoc
@@ -242,8 +242,9 @@ image::form-event-type.png[]
 
 This event is usually used by user interfaces that make it possible to modify 
profile properties, for example a form where a user can edit his profile 
properties, or a management UI to modify.
 
-Note that this event type is a protected event type that is only accepted from 
configured third-party servers, which are
-authorized by source IP address (and in V2 compatibility mode by an 
`X-Unomi-Peer` key valid for that address). That
+Note that this event type is a protected event type that is only accepted from 
authorized source IP addresses (the tenant's
+`authorizedIPs` in normal mode, see <<_secured_events_configuration,Secured 
events configuration>>; in <<_v2_compatibility_mode,single-tenant compatibility 
mode>>
+instead an `X-Unomi-Peer` key valid for the provider's `ipAddresses`). That
 gate is separate from the public-versus-trusted caller distinction that 
governs which profile and session a request may
 bind to — see <<_client_facing_hardening_4_0,client-facing hardening>>. Once 
the event is accepted, the property
 names in `add`, `update`, `addToSet` and `delete` are checked against an 
allowlist. A public-key caller may only write
diff --git a/manual/src/main/asciidoc/condition-validation.adoc 
b/manual/src/main/asciidoc/condition-validation.adoc
index fd4aaebcf..06d310af9 100644
--- a/manual/src/main/asciidoc/condition-validation.adoc
+++ b/manual/src/main/asciidoc/condition-validation.adoc
@@ -184,7 +184,7 @@ Invalid segment condition:
 - Required parameter is missing (parameter: propertyName, condition: 
profilePropertyCondition, location: 
condition[profilePropertyCondition].propertyName)
 ----
 
-REST clients receive this text in the error response body when the save fails.
+REST clients receive a generic `badRequest` response when the save fails. The 
detailed text above is written to the server log.
 
 ==== Rules and invalid conditions
 
diff --git a/manual/src/main/asciidoc/configuration.adoc 
b/manual/src/main/asciidoc/configuration.adoc
index 778f71627..b5de02ecf 100644
--- a/manual/src/main/asciidoc/configuration.adoc
+++ b/manual/src/main/asciidoc/configuration.adoc
@@ -172,11 +172,31 @@ Authentication Rules:
 3. Private paths require both tenantId and private API key
 4. All other requests are denied
 
+[#_secured_events_configuration]
 === Secured events configuration
 
-Apache Unomi secures some events by default. It comes out of the box with a 
default configuration that you can adjust
-by using the centralized configuration file override in 
`$MY_KARAF_HOME/etc/unomi.custom.system.properties`
+Apache Unomi can restrict some event types so only callers from allowed IP 
addresses may send them. How that works
+depends on whether <<_v2_compatibility_mode,single-tenant compatibility mode>> 
is on.
 
+==== Normal multi-tenant mode
+
+In normal 4.0 mode, protected events are controlled with two fields on the 
tenant object, set through REST
+`/cxs/tenants` or `crud update tenant` (where `restrictedEventTypes` is named 
`restrictedEventPermissions`):
+
+* `restrictedEventTypes` — extra event type ids that need an authorized source 
IP (for example `login`)
+* `authorizedIPs` — IP addresses or CIDR ranges allowed to send protected 
events
+
+Some event types are always protected, whatever the tenant lists: 
`sessionCreated`, `sessionReassigned`,
+`profileUpdated`, `ruleFired` and `updateProperties`. Unomi checks the caller 
IP of a protected event against
+`authorizedIPs`. See <<_multitenancy,Multi-tenancy>> for tenant updates.
+
+The global third-party provider files below are **not** used in this mode.
+
+==== Single-tenant compatibility mode (third-party / peer servers)
+
+When single-tenant compatibility mode is enabled, Unomi keeps the Unomi 2 / 
Unomi 3 third-party provider model. Peer
+servers send the secret in the `X-Unomi-Peer` HTTP header. You can adjust the 
defaults with the centralized configuration
+file override in `$MY_KARAF_HOME/etc/unomi.custom.system.properties`.
 
 You can find the default configuration in the following file:
 
@@ -189,7 +209,7 @@ The properties start with the prefix : 
`org.apache.unomi.thirdparty.*` and here
     
org.apache.unomi.thirdparty.provider1.allowedEvents=${env:UNOMI_THIRDPARTY_PROVIDER1_ALLOWEDEVENTS:-login,updateProperties}
 
 The events set in allowedEvents will be secured and will only be accepted if 
the call comes from the specified IP
-address, and if the secret-key is passed in the X-Unomi-Api-Key HTTP request 
header. The "env:" part means that it will
+address, and if the secret is passed in the `X-Unomi-Peer` HTTP request 
header. The "env:" part means that it will
 attempt to read an environment variable by that name, and if it's not found it 
will default to the value after the ":-"
 marker.
 
@@ -223,16 +243,19 @@ the changes when upgrading) :
 
     $MY_KARAF_HOME/etc/org.apache.unomi.thirdparty.cfg
 
-Usually, login events, which operate on profiles and do merge on protected 
properties, must be secured. For each
-trusted third party server, you need to add these 3 lines :
+These provider settings apply only while single-tenant compatibility mode is 
enabled. Usually, login events, which operate
+on profiles and do merge on protected properties, must be secured. For each 
trusted third party server, you need to add
+these 3 lines :
 
 [source]
 ----
-thirdparty.provider1.key=secret-key
+thirdparty.provider1.key=replace-with-a-long-unique-secret
 thirdparty.provider1.ipAddresses=127.0.0.1,::1
 thirdparty.provider1.allowedEvents=login,updateProperties
 ----
 
+Full peer abilities and troubleshooting for this mode are in 
<<_v2_compatibility_mode,single-tenant compatibility mode>>.
+
 
 === Installing the MaxMind GeoIPLite2 IP lookup database
 
@@ -358,16 +381,16 @@ Using Karaf shell (requires admin access to Karaf 
console). See <<_shell_command
 [source,bash]
 ----
 # Create a tenant (JSON properties)
-unomi:crud create tenant -d '{"itemId":"mytenant","name":"My 
Company","description":"My Company tenant"}'
+unomi:crud create tenant '{"itemId":"mytenant","name":"My 
Company","description":"My Company tenant"}'
 
 # List tenants
 unomi:crud list tenant
 
 # View tenant details
-unomi:crud read tenant -i mytenant
+unomi:crud read tenant mytenant
 
 # Delete a tenant
-unomi:crud delete tenant -i mytenant
+unomi:crud delete tenant mytenant
 ----
 
 ==== API Keys and Authentication
@@ -380,7 +403,7 @@ The API keys are generated when creating a tenant, but the 
tenant resource only
 [source,bash]
 ----
 # Using Karaf shell (requires admin access) — metadata only
-unomi:crud read tenant -i mytenant
+unomi:crud read tenant mytenant
 
 # Obtain plaintext (store immediately; it is not persisted)
 curl -X POST "http://localhost:8181/cxs/tenants/mytenant/apikeys?type=PUBLIC"; \
@@ -419,7 +442,7 @@ curl -X POST 
"http://localhost:8181/cxs/tenants/mytenant/apikeys?type=PUBLIC&val
 }
 
 # Using Karaf shell (requires admin access)
-unomi:crud create apikey -d 
'{"tenantId":"mytenant","keyType":"PUBLIC","validityPeriod":30}'
+unomi:crud create apikey 
'{"tenantId":"mytenant","keyType":"PUBLIC","validityPeriod":30}'
 ----
 
 ==== Accessing API Endpoints
@@ -470,10 +493,10 @@ curl -X GET "http://localhost:8181/cxs/tenants"; \
 
 Authentication rules (Unomi 4.0):
 
-1. Public paths (for example `/cxs/context.json`, `/cxs/eventcollector`, 
`/cxs/client/*`) require a valid **public** API key via `X-Unomi-Api-Key` 
(unless <<_v2_compatibility_mode,V2 compatibility mode>> is enabled). Browser 
clients may call them from any origin, but only listed origins and Unomi's own 
origin may use the visitor cookie; see <<_public_tracker_origins,Websites on 
other origins>>.
+1. Public paths (for example `/cxs/context.json`, `/cxs/eventcollector`, 
`/cxs/client/*`) require a valid **public** API key via `X-Unomi-Api-Key` 
(unless <<_v2_compatibility_mode,single-tenant compatibility mode>> is 
enabled). Browser clients may call them from any origin, but only listed 
origins and Unomi's own origin may use the visitor cookie; see 
<<_public_tracker_origins,Websites on other origins>>.
 2. Private paths accept `tenantId:privateApiKey` Basic auth, or JAAS admin 
credentials. With JAAS, `X-Unomi-Tenant-Id` selects the tenant; when it is 
omitted, Unomi uses `org.apache.unomi.rest.authentication.defaultTenantId` (env 
`UNOMI_REST_AUTHENTICATION_DEFAULTTENANTID`, default `default`; not used yet by 
<<_v2_compatibility_mode,single-tenant compatibility mode>>, whose REST 
requests always run on `default`). Send `X-Unomi-Tenant-Id: system` only when a 
full administrator must work i [...]
 3. Tenant administration (`/cxs/tenants`) requires system administrator JAAS 
credentials. Without `X-Unomi-Tenant-Id` it runs in the system context, so it 
works on a fresh install before any tenant exists.
-4. JAAS admin does **not** replace the public API key on public endpoints.
+4. JAAS admin credentials are also accepted on public endpoints in place of 
the public API key; without `X-Unomi-Tenant-Id` they run in the default admin 
tenant (rule 2).
 ==== Public vs Private Endpoints
 
 Public endpoints (requiring only public API key):
diff --git a/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc 
b/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
index 1cde93606..4cc2fac83 100644
--- a/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
+++ b/manual/src/main/asciidoc/migrations/migrate-3.0-to-4.0.adoc
@@ -65,7 +65,7 @@ The main change in 4.0 is the introduction of tenant-based 
authentication. The s
 
 |Event Collection (`/context.json`)
 |None
-|Public API Key only
+|Tenant public API key (or tenant private key / System Admin)
 
 |Administrative Operations
 |System Admin (`karaf:$UNOMI_ROOT_PASSWORD`)
@@ -194,14 +194,14 @@ public void updateKeys(String publicKey, String 
privateKey) {
 }
 ----
 
-==== No API Contract Changes
+==== API paths stay the same; behaviour does not
 
-All API endpoints remain the same between 3.0 and 4.0, and request/response 
payloads are unchanged in shape. The differences are in the authentication 
mechanism, tenant resolution, and which of the identity fields a public caller 
sends to `/context.json` and `/eventcollector` the server acts on — see 
<<_client_facing_hardening_4_0,Client-facing hardening (4.0)>> below.
+API paths stay the same between 3.0 and 4.0, and most request/response 
payloads keep the same shape. Authentication, tenant resolution, and which 
identity fields a public caller may set on `/context.json` and 
`/eventcollector` do change — see <<_client_facing_hardening_4_0,Client-facing 
hardening (4.0)>> below. Other breaking changes are listed in 
<<_breaking_changes_3_0_to_4_0,Breaking changes>>.
 
 [#_client_facing_hardening_4_0]
 ==== Client-facing hardening (4.0)
 
-`/cxs/context.json` and `/cxs/eventcollector` are reachable without 
authentication, or with a tenant *public* API key. In 4.0 the identity fields 
those endpoints accept are treated as claims to be checked rather than as 
instructions.
+`/cxs/context.json` and `/cxs/eventcollector` need a tenant *public* API key 
(or a tenant private key or administrator credentials); without one Unomi 
answers `401`, unless <<_v2_compatibility_mode,single-tenant compatibility 
mode>> is enabled. In 4.0 the identity fields those endpoints accept are 
treated as claims to be checked rather than as instructions.
 
 A caller is **trusted** when it holds a tenant private key or authenticates as 
a system administrator (`ROLE_UNOMI_ADMIN` or `ROLE_UNOMI_TENANT_ADMIN`). A 
tenant *public* API key is **not** trusted: it identifies the tenant, not the 
visitor.
 
@@ -534,6 +534,34 @@ console (port 8102) is not covered by that check.
 - **Compliance**: Easier to meet data privacy regulations with clear tenant 
boundaries
 - **Cost Efficiency**: Shared infrastructure with isolated data reduces 
operational costs
 
+[#_breaking_changes_3_0_to_4_0]
+=== Breaking changes
+
+The sections above cover authentication and client-facing hardening. The lists 
below gather other changes that can break an existing 3.0 installation, 
integration, or plugin. Related tickets: 
https://issues.apache.org/jira/browse/UNOMI-1008[UNOMI-1008] (admin default 
tenant), https://issues.apache.org/jira/browse/UNOMI-1005[UNOMI-1005] (public 
cookie origins), https://issues.apache.org/jira/browse/UNOMI-1007[UNOMI-1007] 
(health check), https://issues.apache.org/jira/browse/UNOMI-1001[UNO [...]
+
+==== Operators
+
+* **Shell commands** such as `profile-list`, `rule-list`, and `segment-view` 
were removed. Use `unomi:crud` instead (for example `crud list profile`, `crud 
read rule RULE_ID`). See <<_shell_commands,Shell commands>>.
+* **Document ids** in the search index now start with the tenant id. Scripts 
or dashboards that read ids directly must be updated.
+* **GraphQL** is enabled through the GraphQL distributions 
(`unomi-distribution-elasticsearch-graphql` or 
`unomi-distribution-opensearch-graphql`) or the `cdp-graphql-feature` feature, 
not the older 3.0 wiring alone. See <<_graphql_api,GraphQL API>>.
+* **Health check password**: `UNOMI_HEALTHCHECK_PASSWORD` no longer defaults 
to `health` and must be set before start (like `UNOMI_ROOT_PASSWORD`). The 
other `UNOMI_HEALTHCHECK_*` variables keep their 3.0 names and defaults. See 
<<_configuration,Configuration>>.
+* **Third-party / peer servers**: `org.apache.unomi.thirdparty.*` and the 
`X-Unomi-Peer` header apply only in <<_v2_compatibility_mode,single-tenant 
compatibility mode>>. In normal multi-tenant mode, protected events are checked 
against each tenant's `authorizedIPs` (plus any extra types in 
`restrictedEventTypes`); see <<_secured_events_configuration,Secured events 
configuration>>.
+* **Migration key file**: `migrate-4.0.0-10-tenantInitialization` writes API 
keys under `{karaf.data}/migration/secrets/`. The public key goes in the 
`X-Unomi-Api-Key` header; the private key is used with HTTP Basic 
authentication as `tenantId:privateKey`. Store the file securely and delete it 
after the keys are saved elsewhere.
+
+==== Integrators / REST clients
+
+* Creating a profile without an `itemId` is refused.
+* Condition validation failures on REST return a generic `badRequest` 
response. Detailed messages stay in the server log. See 
<<_condition_validation,Condition validation>>.
+* Public endpoints need a tenant public API key unless 
<<_v2_compatibility_mode,single-tenant compatibility mode>> is on (for clients 
written for Unomi 2 or Unomi 3). See 
<<_updating_applications_consuming_unomi,Updating applications consuming 
Unomi>> and the client-facing hardening notes above.
+* Websites on another origin must be listed for the visitor cookie; see 
<<_public_tracker_origins,Websites on other origins>>.
+
+==== Plugin developers
+
+* The **task scheduler** is a new service surface (`SchedulerService` and 
related APIs). Plugins that scheduled work their own way should move to it when 
they need cluster-aware tasks. See <<_scheduler,Task scheduler>>.
+* The `pastEventCondition` and `sourceEventPropertyCondition` evaluators moved 
from the base plugin to the advanced-conditions plugin (package 
`org.apache.unomi.plugins.advancedconditions.conditions`). Condition type ids 
are unchanged, so JSON definitions keep working; only Java code that references 
the evaluator classes must be updated.
+* **Query builder ids** were renamed. Legacy `*ESQueryBuilder` ids in 
condition definitions are still mapped to the new ids at run time, with a 
warning in the log, and `migrate-4.0.0-15-updateLegacyQueryBuilder` rewrites 
the stored ones. Update plugin definitions to the new ids to remove the warning.
+* Event, definitions, and profile **Java service** method signatures and 
condition parameter default-value behaviour may differ from 3.0. Rebuild 
plugins against the 4.0 API and run your tests.
+
 === Migration Checklist
 
 Before starting the migration, please ensure that:
@@ -542,9 +570,9 @@ 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,single-tenant compatibility mode>> only if 
coming from 2.x)
-- You have reviewed the <<_client_facing_hardening_4_0,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)
+- You have a plan to update client applications to tenant API keys (or 
temporary <<_v2_compatibility_mode,single-tenant compatibility mode>> for Unomi 
2 or Unomi 3 clients)
+- You have reviewed the <<_breaking_changes_3_0_to_4_0,breaking changes>> and 
<<_client_facing_hardening_4_0,client-facing hardening>> notes
+- You know how to obtain plaintext API keys after upgrade (regenerate via 
`/cxs/tenants/{id}/apikeys`; create responses expose masked keys only; the 
migration also writes a key file under `data/migration/secrets/`)
 - Your recurrent import and export `file` endpoints resolve under 
`{baseDir}/{tenantId}`, or the router's base-directory properties point at the 
directories already in use (see above)
 
 === Migration Process
@@ -569,21 +597,21 @@ Scripts applied for the 4.0 line include (under 
`tools/shell-commands/.../META-I
 * `migrate-4.0.0-05-fixSystemItemIds`
 * `migrate-4.0.0-10-tenantInitialization` (creates the default tenant used for 
isolation)
 * `migrate-4.0.0-15-updateLegacyQueryBuilder`
-4. **Update client applications** to the 4.0 authentication model (public key 
header / tenant private key), or enable <<_v2_compatibility_mode,single-tenant 
compatibility mode>> only when migrating **from Unomi 2.x**.
+4. **Update client applications** to the 4.0 authentication model (public key 
header / tenant private key), or enable <<_v2_compatibility_mode,single-tenant 
compatibility mode>> while Unomi 2 or Unomi 3 clients are still in use.
 5. **Start your Apache Unomi 4.0 cluster**.
-6. **Regenerate and store tenant API keys** (plaintext is returned only from 
the key-creation endpoint), then **test** applications end to end.
+6. **Store tenant API keys** from the migration key file under 
`data/migration/secrets/` (or regenerate via `/cxs/tenants/{id}/apikeys`), then 
**test** applications end to end.
 
 === Transitional authentication options
 
-Unomi 4.0 does **not** provide a separate "3.0 compatibility mode" system 
property. Use one of the following approaches while migrating clients:
+Use one of the following approaches while migrating clients:
 
 ==== Option 1: Update clients to 4.0 authentication (recommended)
 
 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. For the Unomi web tracker, set `apiKey`; 
see <<_web_tracker_server_versions,Configuring the tracker for your Unomi 
server>>.
 
-==== Option 2: single-tenant compatibility mode (2.x clients only)
+==== Option 2: single-tenant compatibility mode (Unomi 2 and Unomi 3 clients)
 
-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:
+If clients were written for **Unomi 2.x or Unomi 3.x**, you can enable 
<<_v2_compatibility_mode,single-tenant compatibility mode>> so they work 
without tenant API keys during a phased rollout:
 
 [source,properties]
 ----
@@ -592,11 +620,11 @@ singletenantcompatibility.enabled = true
 ----
 
 This mode always uses tenant `default`. There is no property to point it at 
another tenant: `defaultTenantId` does not
-apply to it yet, so leave that setting at `default` while the mode is on 
(GraphQL still follows it). If tenant `default` does not exist yet, the first 
public request creates it. Migrated 2.x data has to live in that tenant to be 
visible while the mode is on.
+apply to it yet, so leave that setting at `default` while the mode is on 
(GraphQL still follows it). If tenant `default` does not exist yet, the first 
public request creates it. Migrated data has to live in that tenant to be 
visible while the mode is on.
 
 Environment variable equivalent: 
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true`
 
-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 4.0 authentication.
+WARNING: single-tenant compatibility mode is a temporary bridge for Unomi 2 
and Unomi 3 clients. It bypasses tenant API key requirements on public 
endpoints. Disable it once all clients use 4.0 authentication.
 
 === Post Migration
 
diff --git a/manual/src/main/asciidoc/migrations/migrations.adoc 
b/manual/src/main/asciidoc/migrations/migrations.adoc
index 660a9f1db..4cc8f2b83 100644
--- a/manual/src/main/asciidoc/migrations/migrations.adoc
+++ b/manual/src/main/asciidoc/migrations/migrations.adoc
@@ -17,7 +17,7 @@ This section contains information and steps to migrate 
between major Unomi versi
 Use this decision guide to pick the right runbook:
 
 * **Unomi 2.x → 3.0** (platform): <<_migrate_from_2_x_to_3_0,Migrate from 2.x 
to 3.0>> (+ <<_migrate_from_elasticsearch_7_to_elasticsearch_9,ES7→ES9>> if 
needed)
-* **Unomi 3.0 → 4.0** (tenants / API keys / client hardening): 
<<_migrate_from_3_0_to_4_0,Migrate from 3.0 to 4.0>> (`unomi:migrate`); 
optional <<_v2_compatibility_mode,V2 compatibility mode>> for 2.x clients; see 
also <<_client_facing_hardening_4_0,client-facing hardening>>
+* **Unomi 3.0 → 4.0** (tenants / API keys / client hardening): 
<<_migrate_from_3_0_to_4_0,Migrate from 3.0 to 4.0>> (`unomi:migrate`); 
optional <<_v2_compatibility_mode,single-tenant compatibility mode>> for Unomi 
2 or Unomi 3 clients; see also <<_client_facing_hardening_4_0,client-facing 
hardening>>
 * **Elasticsearch → OpenSearch** (same Unomi version, backend swap): 
<<_migrate_from_elasticsearch_to_opensearch,Migrate from Elasticsearch to 
OpenSearch>> (not `unomi:migrate`)
 
 [plantuml]
@@ -33,10 +33,11 @@ if (Changing Unomi version?) then (yes)
     :ES7 to ES9 data copy if needed;
     if (Need multi-tenancy / API keys?) then (yes)
       :3.0 to 4.0 via unomi:migrate;
-      :Optional V2 compatibility mode;
+      :Optional single-tenant compatibility mode;
     endif
   else (from 3.0)
     :3.0 to 4.0 via unomi:migrate;
+    :Optional single-tenant compatibility mode;
   endif
 else (no)
 endif
@@ -49,7 +50,7 @@ stop
 
 include::writing-migration-scripts.adoc[]
 
-=== V2/V3 API Compatibility Guide
+=== Unomi 2 / 3 to 4.0 API compatibility
 
 include::v2-v3-compatibility.adoc[]
 
diff --git a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc 
b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
index b69621c79..38f4eff9d 100644
--- a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
+++ b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
@@ -15,40 +15,55 @@
 [#_v2_compatibility_mode]
 === single-tenant compatibility mode
 
-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.
+[[_single_tenant_compatibility_mode]]This document explains how to use 
single-tenant compatibility mode in Apache Unomi 4.0. The mode lets client 
applications
+written for Unomi 2.x or Unomi 3.x keep working without tenant API keys while 
you move them to the 4.0 authentication model.
 
 ==== Overview
 
-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.
+The mode is a temporary bridge for Unomi 2 and Unomi 3 clients. It keeps 
public endpoints reachable without API keys, while
+Unomi 4 still stores data under tenants. Every REST request in this mode uses 
tenant `default`.
 
 ===== How It Works
 
 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)
-- **Private endpoints** require system administrator authentication (like V2)
+- **Public endpoints** (like `/context.json`) require no authentication (like 
Unomi 2 and Unomi 3)
+- **Protected events** (like `login`, `updateProperties`) require IP + 
`X-Unomi-Peer` (like Unomi 2 and Unomi 3 third-party servers)
+- **Private endpoints** require system administrator authentication (like 
Unomi 2 and Unomi 3)
 - **Tenant `default`** is used for every operation. The mode cannot be pointed 
at another tenant: it ignores
   `X-Unomi-Tenant-Id` and the `defaultTenantId` setting on REST. GraphQL is 
not affected by the mode and still uses
   `defaultTenantId` for JAAS admin requests without a header, so keep that 
setting at `default` while the mode is on.
   The tenant is created on a public request if it is missing. Each node then 
keeps it in memory for
   about 30 seconds, so public requests do not wait on each other or re-read it 
from the database
   every time. A change made on another node is picked up after about that time.
-- **No authentication** is required for non-protected events (like V2)
-- **The visitor cookie is limited to listed websites** (unlike V2). A website 
on another origin, another subdomain included, must be listed, in 
`UNOMI_PUBLIC_CORS_ALLOWED_ORIGINS` or in the `default` tenant's 
`publicCorsAllowedOrigins` property, for its requests to use and receive the 
cookie, whether it calls `context.json` or embeds `context.js` with a 
`<script>` tag. A plain `<script>` tag (without a `crossorigin` attribute) 
sends no `Origin`, so the listed website is then recognised b [...]
-
-This allows V2 clients to work with Unomi V3 immediately after migration, 
giving you time to gradually update client applications to use the new V3 
authentication model.
+- **No authentication** is required for non-protected events (like Unomi 2 and 
Unomi 3)
+- **The visitor cookie is limited to listed websites** (unlike Unomi 2.x and 
3.x, which sent credentialed CORS headers to every origin; their `SameSite=Lax` 
cookie still
+  reached only pages on the same site).
+  A website on another origin, another subdomain included, must be listed in 
`UNOMI_PUBLIC_CORS_ALLOWED_ORIGINS` or in the
+  `default` tenant's `publicCorsAllowedOrigins` property, for its requests to 
use and receive the cookie, whether it calls
+  `context.json` or embeds `context.js` with a `<script>` tag. A plain 
`<script>` tag (without a `crossorigin` attribute)
+  sends no `Origin`, so the listed website is then recognised by its 
`Referer`, which must be turned on with
+  `org.apache.unomi.public.cors.matchReferer=true` (read 
<<_public_tracker_script_tag,the trade-offs>> first). A website
+  served from Unomi's own origin needs no change. See 
<<_public_tracker_compatibility,Compatibility with Unomi 2.x and 3.x>>.
+
+This allows Unomi 2 and Unomi 3 clients to work with Unomi 4.0 immediately 
after migration, giving you time to update
+applications to tenant API keys.
+
+Unomi 2 and Unomi 3 clients behave the same way in this mode. Third-party / 
peer servers keep using `X-Unomi-Peer` and
+`etc/org.apache.unomi.thirdparty.cfg` while the mode is on; that path is not 
used in normal multi-tenant mode, where the
+tenant's `authorizedIPs` apply instead. See 
<<_secured_events_configuration,Secured events configuration>> and
+https://issues.apache.org/jira/browse/UNOMI-1001[UNOMI-1001].
 
 ==== Prerequisites
 
 Before enabling single-tenant compatibility mode, ensure that:
 
-1. **Data Migration Completed**: Your V2 data has been migrated into tenant 
`default` using the migration scripts. The mode does not read data that lives 
in any other tenant.
-2. **V3 Installation**: Unomi V3 is properly installed and configured. If 
tenant `default` is missing, the first public request creates it.
+1. **Data Migration Completed**: Your data has been migrated into tenant 
`default` using the migration scripts. The mode does not read data that lives 
in any other tenant.
+2. **Unomi 4.0 Installation**: Unomi 4.0 is properly installed and configured. 
If tenant `default` is missing, the first public request creates it.
 
 ==== Configuration
 
-===== Enable V2 Compatibility Mode
+===== Enable single-tenant compatibility mode
 
 You can also set the environment variable 
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true` (maps to 
`org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled` in 
`custom.system.properties`).
 
@@ -91,7 +106,7 @@ unomi:migrate 2.0.0
 
 The `migrate-4.0.0-10-tenantInitialization` script (run as part of that chain 
when migrating through 4.0) creates tenant `default`, which is the only tenant 
this mode uses. If that tenant is missing when the mode is on, the first public 
request creates it. See <<_migrate_from_3_0_to_4_0,Migrate from 3.0 to 4.0>> 
and <<_shell_commands,Shell commands>>.
 
-===== Step 2: Enable V2 Compatibility Mode
+===== Step 2: Enable single-tenant compatibility mode
 
 Enable single-tenant compatibility mode by updating the configuration file:
 
@@ -107,12 +122,12 @@ vi etc/org.apache.unomi.rest.authentication.cfg
 ./bin/start
 ```
 
-===== Step 3: Test V2 Clients
+===== Step 3: Test Unomi 2 / Unomi 3 clients
 
-Your V2 clients should now work without any changes:
+Your existing clients should now work without API key changes:
 
 ```java
-// V2-style authentication still works
+// Unomi 2 / 3 style authentication still works
 RestAssured.authentication = RestAssured.preemptive()
     .basic("karaf", System.getenv("UNOMI_ROOT_PASSWORD"));
 
@@ -126,17 +141,17 @@ RestAssured.given()
 
 ===== Step 4: Gradual Migration
 
-Over time, gradually update your clients to use V3 authentication:
+Over time, gradually update your clients to use 4.0 authentication:
 
 1. **Update client applications** to use API keys
-2. **Test with V3 authentication** while keeping single-tenant compatibility 
mode enabled
+2. **Test with 4.0 authentication** while keeping single-tenant compatibility 
mode enabled
 3. **Disable single-tenant compatibility mode** once all clients are updated
 
 ==== Client Migration Examples
 
-===== From V2 to V3 (with V2 Compatibility Mode)
+===== From Unomi 2 or 3 to Unomi 4 (with compatibility mode)
 
-**V2 Client (continues to work)**:
+**Legacy client (continues to work)**:
 ```java
 // This continues to work in single-tenant compatibility mode
 RestAssured.authentication = RestAssured.preemptive()
@@ -149,9 +164,9 @@ RestAssured.given()
     .post("/context.json");
 ```
 
-**V3 Client (new implementation)**:
+**Unomi 4 client (new implementation)**:
 ```java
-// New V3 client using API keys
+// New 4.0 client using API keys
 given()
     .header("X-Unomi-Api-Key", publicKey)
     .contentType(ContentType.JSON)
@@ -161,22 +176,22 @@ given()
 
 ===== Gradual Migration Strategy
 
-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
+1. **Phase 1**: Enable single-tenant compatibility mode, Unomi 2 / 3 clients 
continue working
+2. **Phase 2**: Develop and test 4.0 clients alongside legacy clients
+3. **Phase 3**: Migrate clients one by one to 4.0 authentication
 4. **Phase 4**: Disable single-tenant compatibility mode once all clients are 
migrated
 
 ==== Security Considerations
 
-===== V2 Compatibility Mode Security
+===== Compatibility mode security
 
 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)
-- **Private endpoints** require system administrator authentication (same as 
V2)
+- **Public endpoints** are accessible without authentication (same as Unomi 2 
and Unomi 3)
+- **Protected events** require IP + `X-Unomi-Peer` authentication (same as 
Unomi 2 / 3 peer servers)
+- **Private endpoints** require system administrator authentication (same as 
Unomi 2 and Unomi 3)
 - **All operations** use tenant `default`
-- **Non-protected events** require no authentication (same as V2)
+- **Non-protected events** require no authentication (same as Unomi 2 and 
Unomi 3)
 
 ===== Protected Events and Peer Abilities
 
@@ -202,7 +217,7 @@ For protected events, clients must:
 
 All other event types are non-protected and require no peer header.
 
-===== V2 Third-Party Configuration
+===== Third-party configuration (compatibility mode only)
 
 Providers are configured in `etc/org.apache.unomi.thirdparty.cfg` with the 
pattern `thirdparty.{providerName}.{property}`:
 
@@ -234,11 +249,11 @@ A provider whose key is empty, shorter than 16 
characters, or equal to the well-
 
 ===== Configuration Management
 
-The V2 third-party configuration supports dynamic updates:
+The third-party configuration supports dynamic updates:
 
 1. **Edit the configuration file**:
    ```bash
-   # Edit the V2 third-party configuration
+   # Edit the third-party configuration
    vi etc/org.apache.unomi.thirdparty.cfg
    ```
 
@@ -280,7 +295,7 @@ Operators should still treat IP allowlists as a second 
factor next to the peer k
 ===== Recommendations
 
 1. **Use single-tenant compatibility mode temporarily** during migration
-2. **Plan for gradual migration** to V3 authentication
+2. **Plan for gradual migration** to 4.0 authentication
 3. **Monitor access patterns** during the transition
 4. **Disable single-tenant compatibility mode** once migration is complete
 5. **Configure non-empty IP allowlists** and minimal `trustedProxies` before 
production use
@@ -289,7 +304,7 @@ Operators should still treat IP allowlists as a second 
factor next to the peer k
 
 ===== Common Issues
 
-**V2 clients still not working**:
+**Legacy clients still not working**:
 - Check configuration file: `etc/org.apache.unomi.rest.authentication.cfg`
 - Verify `singletenantcompatibility.enabled = true`
 - Confirm migrated data is in tenant `default`. The mode cannot be pointed at 
another tenant.
@@ -320,9 +335,9 @@ Check authentication filter logs:
 log:display | grep AuthenticationFilter
 ```
 
-==== Disabling V2 Compatibility Mode
+==== Disabling single-tenant compatibility mode
 
-Once all clients are migrated to V3 authentication:
+Once all clients are migrated to 4.0 authentication:
 
 1. **Update configuration**:
    ```properties
@@ -335,54 +350,24 @@ Once all clients are migrated to V3 authentication:
    ./bin/start
    ```
 
-3. **Verify all clients work** with V3 authentication
+3. **Verify all clients work** with 4.0 authentication
 
 4. **Monitor for any issues** and address them before final deployment
 
-==== Testing V2 Compatibility Mode
-
-The existing test framework supports testing single-tenant compatibility mode 
using system properties.
-
-===== Running Tests in V2 Compatibility Mode
-
-To run tests with single-tenant compatibility mode enabled:
-
-```bash
-# Enable single-tenant compatibility mode for tests
-mvn test -Dunomi.v2.compatibility.mode=true
-
-# Or set the property in your test environment
-export UNOMI_V2_COMPATIBILITY_MODE=true
-mvn test
-```
-
-===== Test Framework Integration
-
-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 single-tenant compatibility mode and normal V3 
mode using the same test suite.
+==== Testing single-tenant compatibility mode
 
-===== Example Test Execution
-
-```bash
-# 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
-mvn test -Dunomi.url=http://localhost:8181
-```
+The integration test `SingleTenantCompatibilityModeIT` (in `itests`) covers 
this mode. It turns the mode on and off at
+runtime through the `singletenantcompatibility.enabled` setting of 
`org.apache.unomi.rest.authentication`, so it runs
+as part of the normal integration test suite; no extra system property is 
needed.
 
 ==== Conclusion
 
-The single-tenant 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 2 or Unomi 3 to Unomi 4, allowing you to:
 
-- **Maintain existing V2 clients** during migration
-- **Gradually migrate** to V3 authentication
-- **Leverage V3 features** while maintaining backward compatibility
+- **Maintain existing clients** during migration
+- **Gradually migrate** to 4.0 authentication
+- **Leverage Unomi 4 features** while maintaining backward compatibility
 - **Minimize downtime** during the migration process
-- **Test both modes** using the existing test framework
+- **Test both modes** with the integration test suite
 
-Use this mode as a temporary solution during your migration journey, and plan 
to disable it once all clients are updated to use V3 authentication.
+Use this mode as a temporary solution during your migration journey, and plan 
to disable it once all clients are updated to use 4.0 authentication.
diff --git a/manual/src/main/asciidoc/migrations/v2-v3-compatibility.adoc 
b/manual/src/main/asciidoc/migrations/v2-v3-compatibility.adoc
index 6a5aa83ae..d92236528 100644
--- a/manual/src/main/asciidoc/migrations/v2-v3-compatibility.adoc
+++ b/manual/src/main/asciidoc/migrations/v2-v3-compatibility.adoc
@@ -12,9 +12,9 @@
 // limitations under the License.
 //
 
-=== Apache Unomi V2/V3 API Differences Guide
+=== Apache Unomi 2 / 3 to 4.0 API differences
 
-This document explains the key differences between Apache Unomi 2.x and 3.x 
versions from an API perspective.
+This document explains the key API differences between Apache Unomi 2.x / 3.x 
and Unomi 4.0.
 
 ==== Overview
 
@@ -31,13 +31,13 @@ The key innovation in Unomi 4.0 is the introduction of 
**tenant isolation** for
 
 This multi-tenancy support necessitates the authentication changes described 
below, as the system must now identify which tenant context to operate in for 
every request.
 
-==== Key Differences Between V2 and V3
+==== Key differences between Unomi 2 / 3 and Unomi 4
 
 ===== Authentication Model
 
 [cols="1,1,1", options="header"]
 |===
-|Aspect |Unomi V2 |Unomi 4.0
+|Aspect |Unomi 2 / 3 |Unomi 4.0
 
 |Authentication Method
 |System Administrator Authentication (`karaf` user)
@@ -56,9 +56,9 @@ This multi-tenancy support necessitates the authentication 
changes described bel
 |System Administrator Authentication (`karaf` user)
 |===
 
-===== API Key Types (V3 Only)
+===== API Key Types (4.0 only)
 
-V3 introduces two types of API keys per tenant:
+Unomi 4.0 introduces two types of API keys per tenant:
 
 - **Public Key**: Used for public endpoints (event collection via 
`/context.json`)
 - **Private Key**: Used with tenantId for tenant-specific administrative 
operations
@@ -67,7 +67,7 @@ V3 introduces two types of API keys per tenant:
 
 [cols="1,1,1", options="header"]
 |===
-|Endpoint Category |V2 Authentication |V3 Authentication
+|Endpoint Category |Unomi 2 / 3 Authentication |Unomi 4.0 Authentication
 
 |Event Collection (`/context.json`)
 |None
@@ -82,9 +82,9 @@ V3 introduces two types of API keys per tenant:
 |System Admin (`karaf` user)
 |===
 
-==== Authentication Flow (V3)
+==== Authentication Flow (4.0)
 
-The AuthenticationFilter in V3 follows this resolution order:
+The AuthenticationFilter in 4.0 follows this resolution order:
 
 1. **Tenant endpoints** (`/cxs/tenants`): Requires system administrator 
authentication only
 2. **Public endpoints** (e.g., `/context.json`): Requires public API key via 
`X-Unomi-Api-Key` header
@@ -94,7 +94,7 @@ The AuthenticationFilter in V3 follows this resolution order:
 
 ==== Code Examples
 
-===== V2 Authentication
+===== Unomi 2 / 3 Authentication
 
 [source,java]
 ----
@@ -110,7 +110,7 @@ RestAssured.given()
     .post("/context.json");
 ----
 
-===== V3 Authentication
+===== Unomi 4.0 Authentication
 
 [source,java]
 ----
@@ -151,12 +151,12 @@ given()
 ----
 public class UnomiConfiguration {
     public UnomiClient createClient(String baseUrl) {
-        String version = System.getProperty("unomi.version", "3");
+        String version = System.getProperty("unomi.version", "4.0");
         
-        if ("3".equals(version)) {
-            return new UnomiV3Client(baseUrl);
+        if ("4.0".equals(version)) {
+            return new UnomiV40Client(baseUrl);
         } else {
-            return new UnomiV2Client(baseUrl);
+            return new UnomiLegacyClient(baseUrl);
         }
     }
 }
@@ -166,14 +166,14 @@ public class UnomiConfiguration {
 
 [source,java]
 ----
-// V2 Client
+// Unomi 2 / 3 Client
 public void init() {
     RestAssured.baseURI = baseUrl;
     RestAssured.authentication = RestAssured.preemptive()
         .basic("karaf", System.getenv("UNOMI_ROOT_PASSWORD"));
 }
 
-// V3 Client
+// Unomi 4.0 Client
 public void init() {
     RestAssured.baseURI = baseUrl;
 }
@@ -186,7 +186,7 @@ public void updateKeys(String publicKey, String privateKey) 
{
 
 ==== Migration Guidelines
 
-===== From V2 to V3
+===== From Unomi 2 or 3 to Unomi 4
 
 1. **Understand Multi-Tenancy Impact**
    - All data (profiles, events, segments, rules, schemas) becomes 
tenant-specific
@@ -194,8 +194,8 @@ public void updateKeys(String publicKey, String privateKey) 
{
    - Tenant context must be established for every API operation
 
 2. **Update Authentication Configuration**
-   - Remove global system administrator authentication
    - Configure tenant-specific public and private API keys
+   - Or, while clients are being updated, enable 
<<_v2_compatibility_mode,single-tenant compatibility mode>>
    - Implement endpoint-specific authentication logic
 
 3. **Endpoint-Specific Changes**
@@ -204,12 +204,12 @@ public void updateKeys(String publicKey, String 
privateKey) {
    - Keep system administrator authentication as fallback for administrative 
operations
    - Continue using system administrator authentication for tenant 
administration
 
-4. **No API Contract Changes**
-   - All endpoints remain the same
-   - Request/response payloads are unchanged
-   - Only authentication mechanism differs
+4. **API paths stay the same**
+   - Endpoint paths remain the same
+   - Most request/response payloads keep the same shape
+   - Authentication and tenant resolution differ; see the 3.0→4.0 migration 
guide for other breaking changes
 
-===== Benefits of Multi-Tenancy in V3
+===== Benefits of Multi-Tenancy in 4.0
 
 - **Data Isolation**: Complete separation ensures tenant data never crosses 
boundaries
 - **Scalability**: Support for multiple customers/organizations in a single 
Unomi instance
@@ -219,12 +219,12 @@ public void updateKeys(String publicKey, String 
privateKey) {
 
 ==== Conclusion
 
-The fundamental difference between Unomi V2 and V3 is the introduction of 
**comprehensive multi-tenancy support**:
+The fundamental difference between Unomi 2 / 3 and Unomi 4 is the introduction 
of **comprehensive multi-tenancy support**:
 
-- **V2**: Single-tenant architecture with system administrator authentication 
for all operations
-- **V3**: Multi-tenant architecture with complete data isolation and 
tenant-specific authentication
-- **API Endpoints**: Identical between versions - no breaking changes to 
existing integrations
-- **Data Model**: All entities (profiles, events, segments, rules, schemas) 
become tenant-specific in V3
+- **Unomi 2 / 3**: Single-tenant architecture with system administrator 
authentication for all operations
+- **Unomi 4**: Multi-tenant architecture with complete data isolation and 
tenant-specific authentication
+- **API Endpoints**: Paths stay the same; authentication and some behaviours 
change (see the migration guide)
+- **Data Model**: All entities (profiles, events, segments, rules, schemas) 
become tenant-specific in 4.0
 - **Authentication**: New tenant-based authentication model with system 
administrator authentication as fallback
 
-The authentication changes in V3 are driven by the need to establish tenant 
context for every operation, ensuring complete data isolation while maintaining 
backward compatibility for administrative operations. 
\ No newline at end of file
+The authentication changes in 4.0 are driven by the need to establish tenant 
context for every operation, ensuring complete data isolation while maintaining 
backward compatibility for administrative operations. 
\ No newline at end of file
diff --git a/manual/src/main/asciidoc/privacy.adoc 
b/manual/src/main/asciidoc/privacy.adoc
index e929ec57c..0a430e578 100644
--- a/manual/src/main/asciidoc/privacy.adoc
+++ b/manual/src/main/asciidoc/privacy.adoc
@@ -64,9 +64,9 @@ curl -X GET http://localhost:8181/cxs/client/myprofile/json \
 --cookie "context-profile-id=PROFILE-ID"
 ----
 
-where the last path segment is the format (`json`, `csv`, `yaml` or `text`), 
`PROFILE-ID` is the profile identifier for which to download the profile, and 
`YOUR_PUBLIC_API_KEY` is the tenant public API key (required on Unomi 4.0 
public `/cxs/client/*` endpoints unless <<_v2_compatibility_mode,V2 
compatibility mode>> is enabled).
+where the last path segment is the format (`json`, `csv`, `yaml` or `text`), 
`PROFILE-ID` is the profile identifier for which to download the profile, and 
`YOUR_PUBLIC_API_KEY` is the tenant public API key (required on Unomi 4.0 
public `/cxs/client/*` endpoints unless <<_v2_compatibility_mode,single-tenant 
compatibility mode>> is enabled).
 
-In V2 compatibility mode a plain link on the website to this endpoint also 
works, as a top-level navigation; see <<_public_tracker_origins,Websites on 
other origins>>.
+In single-tenant compatibility mode a plain link on the website to this 
endpoint also works, as a top-level navigation; see 
<<_public_tracker_origins,Websites on other origins>>.
 
 === Deleting a profile
 
diff --git a/manual/src/main/asciidoc/recipes.adoc 
b/manual/src/main/asciidoc/recipes.adoc
index 513c64616..d974ca630 100644
--- a/manual/src/main/asciidoc/recipes.adoc
+++ b/manual/src/main/asciidoc/recipes.adoc
@@ -134,8 +134,9 @@ event data to the profile. This is simpler than it sounds, 
as usually all it req
 defining the corresponding JSON schema and you're ready to update profiles 
using events. 
 
 - Use the protected built-in "updateProperties" event. This event is designed 
to be used for administrative purposes
-only. Being a protected event type, it is only accepted from an authorized 
source address — a configured third-party
-server, or in V2 compatibility mode an `X-Unomi-Peer` key valid for that 
source address. Note that this is an
+only. Being a protected event type, it is only accepted from an authorized 
source address — the tenant's
+`authorizedIPs` (it is always protected, whatever the tenant's 
`restrictedEventTypes`), or in <<_v2_compatibility_mode,single-tenant 
compatibility mode>>
+an `X-Unomi-Peer` key valid for that source address. Note that this is an
 address-based gate: it is independent of the public-versus-trusted caller 
distinction that governs profile and session
 binding (see <<_client_facing_hardening_4_0,client-facing hardening>>), and 
once the event is accepted a public-key
 caller may only write plain `properties.*` paths on the current profile; 
`targetId`, `systemProperties`, `segments`,
diff --git a/manual/src/main/asciidoc/security.adoc 
b/manual/src/main/asciidoc/security.adoc
index a459d94e0..ed7293413 100644
--- a/manual/src/main/asciidoc/security.adoc
+++ b/manual/src/main/asciidoc/security.adoc
@@ -44,7 +44,7 @@ or not the tenant exists. For tenant data, the system context 
is never the silen
 (`/cxs/tenants`) is the exception: it manages the tenant registry, so a full 
administrator without the header works in
 the system context, even before any tenant exists.
 
-Temporary exception: <<_v2_compatibility_mode,V2 compatibility mode>> allows 
public endpoints without API keys while migrating 2.x clients.
+Temporary exception: <<_v2_compatibility_mode,single-tenant compatibility 
mode>> allows public endpoints without API keys while migrating Unomi 2 or 
Unomi 3 clients.
 In that mode every REST request, JAAS admin requests included, runs on tenant 
`default` and the default tenant setting
 is not used yet; GraphQL still follows the setting, so keep it at `default` 
while the mode is on.
 
@@ -76,9 +76,9 @@ else Private path + Basic tenantId:privateKey
 else Private path + JAAS
   Auth -> Tenant : X-Unomi-Tenant-Id or admin default tenant
   Auth -> API : Tenant context (system only with explicit header)
-else V2 compatibility mode
+else single-tenant compatibility mode
   Auth -> Tenant : Compatibility tenant
-  Auth -> API : Legacy 2.x-style access
+  Auth -> API : Legacy Unomi 2 / 3 style access
 else
   Auth --> Client : 401 Unauthorized
 end
@@ -146,7 +146,7 @@ end note
 
 Configuration keys live in `etc/org.apache.unomi.rest.authentication.cfg` and 
`custom.system.properties`. Tenant API key settings are in 
`etc/org.apache.unomi.tenant.cfg`.
 
-For tenant API keys, roles (`ROLE_UNOMI_TENANT_USER`, 
`ROLE_UNOMI_TENANT_ADMIN`, system administrator), operation permissions, and 
curl examples, see <<_multitenancy,Multi-tenancy>> and 
<<_configuration,Configuration>> (tenant management). For migration from 2.x, 
see <<_v2_compatibility_mode,V2 compatibility mode>> and 
<<_migrate_from_3_0_to_4_0,Migrate from 3.0 to 4.0>>.
+For tenant API keys, roles (`ROLE_UNOMI_TENANT_USER`, 
`ROLE_UNOMI_TENANT_ADMIN`, system administrator), operation permissions, and 
curl examples, see <<_multitenancy,Multi-tenancy>> and 
<<_configuration,Configuration>> (tenant management). For migrating Unomi 2 or 
Unomi 3 clients, see <<_v2_compatibility_mode,single-tenant compatibility 
mode>> and <<_migrate_from_3_0_to_4_0,Migrate from 3.0 to 4.0>>.
 
 ===== Multi-tenancy isolation
 
diff --git a/manual/src/main/asciidoc/shell-commands.adoc 
b/manual/src/main/asciidoc/shell-commands.adoc
index b4ea60626..bd25cef41 100644
--- a/manual/src/main/asciidoc/shell-commands.adoc
+++ b/manual/src/main/asciidoc/shell-commands.adoc
@@ -92,23 +92,28 @@ using the specified distribution feature name 
(unomi-distribution-elasticsearch
 
 These commands are available once the application is running. If an argument 
is between brackets [] it means it is optional.
 
+Most list/view/remove operations on profiles, events, rules, segments, 
sessions, actions and conditions now go through
+`unomi:crud`. Set the tenant first with `tenant-set` (see 
<<_tenant_commands,Tenant commands>>). Use `unomi:crud help TYPE`
+for the fields each type accepts.
+
 .Table Runtime commands
 |===
 |Command|Arguments|Description
 
-|rule-list
-|[--csv] [maxEntries]
-|Lists all the rules registered in the Apache Unomi server. The maxEntries 
(defaults to 100) will allow you to specify
-how many entries need to be retrieved. If the value is inferior to the total 
value, a message will display the total
-value of rules registered in the server. If you add the "--csv" option the 
list will be output as a CSV formatted table.
-Note: Options must come before arguments, so use `rule-list --csv 100` not 
`rule-list 100 --csv`.
-|rule-view
-|rule-id
-|Dumps a single rule in JSON. The rule-id argument can be retrieved from the 
`rule-list` command output.
-|rule-remove
-|rule-id
-|Removes a single rule from Apache Unomi. The `rule-id` argument can be 
retrieved from the `rule-list` command output.
-Warning: no confirmation is asked, be careful with this command.
+|crud list
+|type [[--csv] [-n maxEntries]]
+|Lists items of the given type for the current tenant. Common types: 
`profile`, `event`, `rule`, `segment`, `session`,
+`actiontype`, `conditiontype`. Example: `crud list profile` or `crud list rule 
--csv`.
+|crud read
+|type item-id
+|Dumps one item as JSON. Example: `crud read profile PROFILE_ID` or `crud read 
rule RULE_ID`.
+|crud delete
+|type item-id
+|Deletes one item. Warning: no confirmation is asked. Example: `crud delete 
profile PROFILE_ID`.
+|crud create / crud update
+|type JSON-or-file-URI [for update: item-id then JSON]
+|Create or update an item from a JSON string or a `file://` path. Use `crud 
help TYPE` for required fields.
+
 |rule-reset-stats
 |n/a
 |Resets the rule statistics. This is notably useful when trying to understand 
rule performance and impact
@@ -129,64 +134,6 @@ created, EXECUTE means the rule's actions are being 
executed.
 |event-tail
 |n/a
 |Dumps any incoming events to the Apache Unomi server to the console. Use 
CTRL+C to exit tail
-|event-view
-|event-id
-|Dumps a single event in JSON. The `event-id` can be retrieved from the 
event-tail command output.
-|event-list
-|[--csv] [max-entries] [event-type]
-|List the last events processed by Apache Unomi. The `max-entries` parameter 
can be used to control how many events are
-displayed (default is 100). The `event-type` makes it possible to filter the 
list by event type. The `--csv` argument is used to output the list as a CSV 
list instead of an ASCII table.
-Note: Options must come before arguments, so use `event-list --csv 100 view` 
not `event-list 100 view --csv`.
-Note: Options must come before arguments, so use `event-list --csv 100 view` 
not `event-list 100 view --csv`.
-|event-search
-|profile-id [event-type] [max-entries]
-|This command makes it possible to search for the last events by `profile-id` 
and by `event-type`. A `max-entries`
-parameter (with a default value of 100) is also accepted to control the number 
of results returned by the search.
-
-|action-list
-|[--csv]
-|Lists all the rule actions registered in the Apache Unomi server. This 
command is useful when developing plugins to
-check that everything is properly registered. If you add the "--csv" option 
the list will be output as a CSV formatted table
-|action-view
-|action-id
-|Dumps a single action in JSON. The action-id argument can be retrieved from 
the `action-list` command output.
-
-|condition-list
-|[--csv]
-|List all the conditions registered in the server. If you add the "--csv" 
option the list will be output as a CSV formatted table
-|condition-view
-|condition-id
-|Dumps a single condition in JSON. The condition-id can be retrieved from the 
`condition-list` command output.
-
-|profile-list
-|[--csv]
-|List the last 10 modified profiles. If you add the "--csv" option the list 
will be output as a CSV formatted table
-|profile-view
-|profile-id
-|Dumps a single profile in JSON. The profile-id argument can be retrieved from 
the `profile-list` command output.
-|profile-remove
-|profile-id
-|Removes a profile identified by `profile-id` argument. Warning: no 
confirmation is asked so be careful with this command!
-
-|segment-list
-|[--csv]
-|Lists all the segments registered in the Apache Unomi server. If you add the 
"--csv" option the list will be output as a CSV formatted table
-|segment-view
-|segment-id
-|Dumps a single segment in JSON. The segment-id argument can be retrieved from 
the `segment-list` command output.
-|segment-remove
-|segment-id
-|Removes a single segment identified by the `segment-id` argument. Warning: no 
confirmation is asked so be careful with
-this command!
-
-|session-list
-|[--csv]
-|Lists the last 10 sessions by last event date. If you add the "--csv" option 
the list will be output
-                                                as a CSV formatted table
-|session-view
-|session-id
-|Dumps a single session in JSON. The session-id argument can be retrieved from 
the `session-list`, `profile-list` or
- `event-tail` command output.
 
 |deploy-definition
 |[bundleId] [type] [fileName]
@@ -207,6 +154,7 @@ command: `undeploy-definition BUNDLE_ID * *` when 
`BUNDLE_ID` is the identifier
 |===
 
 
+[#_tenant_commands]
 ==== Tenant commands
 
 Multi-tenant shell operations (Unomi 4.0+). See 
<<_multitenancy,Multi-tenancy>> for REST equivalents.
@@ -228,19 +176,19 @@ Multi-tenant shell operations (Unomi 4.0+). See 
<<_multitenancy,Multi-tenancy>>
 |List tenants (excludes the built-in `system` tenant from normal listings).
 
 |crud read tenant
-|-i tenant-id
+|tenant-id
 |Show one tenant as JSON.
 
 |crud create tenant
-|-d JSON or --file path
-|Create a tenant. Use `help` on the command for required properties.
+|JSON or `file://` path
+|Create a tenant. Use `crud help tenant` for the accepted properties.
 
 |crud update tenant
-|-i tenant-id -d JSON or --file path
-|Update tenant metadata.
+|tenant-id JSON or `file://` path
+|Update tenant metadata. The shell names the restricted event types property 
`restrictedEventPermissions`.
 
 |crud delete tenant
-|-i tenant-id
+|tenant-id
 |Delete a tenant.
 
 |crud list apikey
@@ -248,7 +196,7 @@ Multi-tenant shell operations (Unomi 4.0+). See 
<<_multitenancy,Multi-tenancy>>
 |List API keys across all tenants (keys are masked in the table).
 
 |crud create apikey
-|-d JSON
+|JSON
 |Create an API key for a tenant (`tenantId`, `keyType`, optional 
`validityPeriod`).
 
 |cache
diff --git a/manual/src/main/asciidoc/tutorial.adoc 
b/manual/src/main/asciidoc/tutorial.adoc
index 73e933336..20088e1f2 100644
--- a/manual/src/main/asciidoc/tutorial.adoc
+++ b/manual/src/main/asciidoc/tutorial.adoc
@@ -211,17 +211,18 @@ Another (powerful) way to look at events is to use the 
SSH Console. You can conn
 Using the same credentials (the `karaf` user and your configured 
`UNOMI_ROOT_PASSWORD`) and then you can use command such as :
 
 - `event-tail` to view in realtime the events as they come in (CTRL+C to stop)
-- `event-list` to view the latest events
-- `event-view EVENT_ID` to view the details of a specific event
+- `tenant-set default` then `crud list event` to view recent events for that 
tenant
+- `crud read event EVENT_ID` to view the details of a specific event
 
 ==== Viewing the current profile
 
-By default, Unomi uses a cookie called context-profile-id to keep track of the 
current profile. You can use this the value of this cookie which contains a 
UUID to lookup the details of the profile. For example with the SSH console you 
can simply to:
+By default, Unomi uses a cookie called context-profile-id to keep track of the 
current profile. You can use this the value of this cookie which contains a 
UUID to lookup the details of the profile. For example with the SSH console you 
can simply do:
 
-    profile-view PROFILE_UUID
+    tenant-set default
+    crud read profile PROFILE_UUID
 
 Which will print out the details of the profile with the associated ID.
-Another interesting command is `profile-list` to list all the recently 
modified profiles
+Another interesting command is `crud list profile` to list recently modified 
profiles for the current tenant
 
 You could also retrieve the profile details using the REST API by using a 
request such as this one:
 
@@ -267,9 +268,9 @@ curl --location --request POST 
'http://localhost:8181/cxs/rules' \
 
 The above rule will execute when a view event is received (which is 
automatically sent by the tracker when a page is loaded) and increments a 
property called `pageViewCount` on the user's profile.
 
-You can then reload then page and check with the `profile-view PROFILE_UUID` 
SSH command that the profile was updated with the new property and that it is 
incremented on each page reload.
+You can then reload the page and check with `crud read profile PROFILE_UUID` 
that the profile was updated with the new property and that it is incremented 
on each page reload.
 
-You can also use the `rule-list` command to display all the rules in the 
system and the `rule-tail` to watch in real-time which rules are executed. The 
`rule-view RULE_ID` command will let you view the contents of a rule.
+You can also use `crud list rule` to display the rules for the current tenant 
and `rule-tail` to watch in real-time which rules are executed. `crud read rule 
RULE_ID` shows the contents of a rule.
 
 ==== Adding personalization
 
diff --git a/manual/src/main/asciidoc/whats-new.adoc 
b/manual/src/main/asciidoc/whats-new.adoc
index df268e6fa..65fa44f40 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_4_0,Migrate from 3.0 to 4.0>> (includes 
<<_client_facing_hardening_4_0,client-facing hardening>>)
-* Migrating from Unomi 2.x: <<_v2_compatibility_mode,single-tenant 
compatibility mode>> (`singletenantcompatibility.enabled` in 
`org.apache.unomi.rest.authentication.cfg`)
+* Migrating Unomi 2 or Unomi 3 clients: <<_v2_compatibility_mode,single-tenant 
compatibility mode>> (`singletenantcompatibility.enabled` in 
`org.apache.unomi.rest.authentication.cfg`)
 * Public and GraphQL API key checks no longer read every tenant on each 
request, and refuse revoked and expired keys 
(https://issues.apache.org/jira/browse/UNOMI-1016[UNOMI-1016])
 * JAAS admin requests without `X-Unomi-Tenant-Id` use a configurable default 
tenant (default `default`), so 3.0 admin scripts keep seeing migrated data 
(https://issues.apache.org/jira/browse/UNOMI-1008[UNOMI-1008])
 * Health check environment variables `UNOMI_HEALTHCHECK_ENABLED`, 
`UNOMI_HEALTHCHECK_PROVIDERS` and `UNOMI_HEALTHCHECK_TIMEOUT` work again 
(https://issues.apache.org/jira/browse/UNOMI-1007[UNOMI-1007])
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 126102488..0598fd47e 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
@@ -294,16 +294,16 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
     /**
      * 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
+     * - Public endpoints (like /context.json) require no authentication (like 
Unomi 2 / 3)
+     * - Protected events require IP + X-Unomi-Peer (like Unomi 2 / 3)
+     * - Private endpoints require system administrator authentication (like 
Unomi 2 / 3)
+     * - Tenant {@code default} is used for all REST operations
      */
     private void handleSingleTenantCompatibilityMode(ContainerRequestContext 
requestContext, String path) throws IOException {
-        // For public paths, allow access without authentication (like V2)
+        // For public paths, allow access without authentication (like Unomi 2 
/ 3)
         if (isPublicPath(requestContext)) {
             String tenantId = COMPATIBILITY_TENANT_ID;
-            // V2 knew no tenant, so a V2 client cannot name one and cannot 
create one. Create the
+            // Legacy Unomi 2 / 3 clients do not name a tenant and cannot 
create one. Create the
             // tenant this mode runs on, rather than refuse every request 
until an operator creates
             // it by hand.
             try {
@@ -327,7 +327,7 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
             return;
         }
 
-        // For private endpoints, require system administrator authentication 
(like V2)
+        // For private endpoints, require system administrator authentication 
(like Unomi 2 / 3)
         String authHeader = 
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
         if (authHeader != null && authHeader.startsWith(BASIC_AUTH_PREFIX)) {
             if (rejectBlankBasicAuthPassword(requestContext, authHeader)) {
@@ -346,7 +346,7 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
                 Subject jaasSubject = ((RolePrefixSecurityContextImpl) 
securityContext).getSubject();
 
                 // Private endpoints in single-tenant compatibility mode 
require system administrator
-                // authentication (like V2) — a JAAS login alone isn't enough, 
since any Karaf
+                // authentication (like Unomi 2 / 3) — 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("single-tenant compatibility mode: 
authenticated user lacks administrator role, denying access to private 
endpoint");
@@ -392,8 +392,8 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
      * (notably {@code karaf.bat}), so an empty credential is never accepted 
over REST either.
      * <p>
      * Deliberately called at each point where a Basic credential is actually 
consumed rather than
-     * once at the top of {@link #filter}: anonymous traffic on public paths — 
and every path in V2
-     * compatibility mode — ignores {@code Authorization} entirely, so 
rejecting up front would turn
+     * once at the top of {@link #filter}: anonymous traffic on public paths — 
in normal and in
+     * single-tenant compatibility mode — ignores {@code Authorization} 
entirely, so rejecting up front would turn
      * a stray or stale Basic header (a cached browser credential, an 
injecting proxy) into a 401 on
      * a request that is supposed to succeed without authentication.
      *
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 0d030f161..52c4e59f3 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
@@ -64,10 +64,10 @@ public interface RestAuthenticationConfig extends 
AdminTenantConfig {
 
     /**
      * 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
+     * When enabled, Unomi 2 and Unomi 3 clients can use Unomi 4.0 without 
requiring API keys:
+     * - Public endpoints (like /context.json) require no authentication (like 
Unomi 2 / 3)
+     * - Private endpoints require system administrator authentication (like 
Unomi 2 / 3)
+     * - Tenant {@code default} is used for all REST operations
      *
      * @return true if single-tenant compatibility mode is enabled, false 
otherwise
      */
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
index 84d87d4a7..3dffe258a 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
@@ -58,7 +58,7 @@ public class V2ThirdPartyConfigService {
     private static final String PROP_TRUSTED_PROXIES = "trustedProxies";
 
     /**
-     * OSGi configuration for V2 third-party providers.
+     * OSGi configuration for third-party providers (single-tenant 
compatibility mode).
      */
     @ObjectClassDefinition(
         name = "Apache Unomi Third-Party Configuration",
@@ -232,7 +232,7 @@ public class V2ThirdPartyConfigService {
         int totalEvents = newProviders.values().stream()
                 .mapToInt(config -> config.getAllowedEvents().size())
                 .sum();
-        LOGGER.info("V2 Third-Party Configuration updated - {} providers with 
{} total protected events"
+        LOGGER.info("Third-party configuration updated - {} providers with {} 
total protected events"
                         + " (allowExampleKey={}, trustedProxies={})",
                 newProviders.size(), totalEvents, allowExample, 
proxies.size());
     }
@@ -311,7 +311,7 @@ public class V2ThirdPartyConfigService {
 
         Optional<ProviderConfig> match = findProviderByKey(providerKey);
         if (match.isEmpty()) {
-            LOGGER.debug("V2 compatibility mode: Unknown provider key: {}", 
SecurityUtils.maskSecret(providerKey));
+            LOGGER.debug("single-tenant compatibility mode: Unknown provider 
key: {}", SecurityUtils.maskSecret(providerKey));
             return false;
         }
 
@@ -319,14 +319,14 @@ public class V2ThirdPartyConfigService {
         String foundProviderId = config.getProviderId();
 
         if (!config.getAllowedEvents().contains(eventType)) {
-            LOGGER.debug("V2 compatibility mode: Event type {} not allowed for 
provider {} (key: {})",
+            LOGGER.debug("single-tenant compatibility mode: Event type {} not 
allowed for provider {} (key: {})",
                     eventType, foundProviderId, 
SecurityUtils.maskSecret(providerKey));
             return false;
         }
 
         boolean ipAuthorized = isIpAllowed(config, sourceIP);
         if (!ipAuthorized) {
-            LOGGER.debug("V2 compatibility mode: IP {} not authorized for 
provider {} (key: {})",
+            LOGGER.debug("single-tenant compatibility mode: IP {} not 
authorized for provider {} (key: {})",
                     sourceIP, foundProviderId, 
SecurityUtils.maskSecret(providerKey));
         }
         return ipAuthorized;
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 2e87437dd..caa989457 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
@@ -662,20 +662,20 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
 
     /**
      * Check if an event is allowed in single-tenant compatibility mode.
-     * In V2, protected events required IP + X-Unomi-Peer (third-party key) 
authentication.
+     * As in Unomi 2 and 3, protected events require IP + X-Unomi-Peer 
(third-party key) authentication.
      *
      * @param event the event to check
      * @param request the HTTP request
      * @return true if the event is allowed, false otherwise
      */
     private boolean isEventAllowedInV2CompatibilityMode(Event event, 
HttpServletRequest request) {
-        // Check if this is a protected event type using the V2 third-party 
configuration
+        // Check if this is a protected event type using the third-party 
configuration
         if 
(!v2ThirdPartyConfigService.isProtectedEventType(event.getEventType())) {
             // Non-protected events are always allowed in single-tenant 
compatibility mode
             return true;
         }
 
-        // For protected events, check IP + third-party key (V2-style)
+        // For protected events, check IP + third-party key (as in Unomi 2 and 
3)
         String sourceIP = v2ThirdPartyConfigService.resolveClientIp(request);
         String thirdPartyKey = request.getHeader("X-Unomi-Peer");
 
@@ -684,7 +684,7 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
             return false;
         }
 
-        // Validate the third-party provider using the V2 configuration
+        // Validate the third-party provider using the third-party 
configuration
         if (!v2ThirdPartyConfigService.validateProviderByKey(thirdPartyKey, 
event.getEventType(), sourceIP)) {
             LOGGER.debug("single-tenant compatibility mode: Protected event {} 
rejected - invalid third-party provider key: {} from IP: {}",
                         event.getEventType(), 
SecurityUtils.maskSecret(thirdPartyKey), sourceIP);
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 a18dae6ac..bba44989e 100644
--- a/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
+++ b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
@@ -17,11 +17,11 @@
 # Unomi REST Authentication Configuration
 # This file configures authentication settings for Unomi REST endpoints
 
-# V2 Compatibility Mode
-# When enabled, allows V2 clients to 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
+# Single-tenant compatibility mode
+# When enabled, clients written for Unomi 2 or Unomi 3 can use Unomi 4.0 
without API keys
+# - Public endpoints (like /context.json) require no authentication (like 
Unomi 2 / 3)
+# - Private endpoints require system administrator authentication (like Unomi 
2 / 3)
+# - Tenant default is used for all REST operations
 singletenantcompatibility.enabled = 
${org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled:-false}
 
 # Default tenant (UNOMI-1008)
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 fa707202a..c4a97e9bb 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
@@ -143,9 +143,9 @@ class AuthenticationFilterBlankPasswordTest {
     }
 
     /**
-     * The ordinary V3 branch: not {@code tenants}, not a public path, V2 
compatibility off. This is
+     * The ordinary multi-tenant branch: not {@code tenants}, not a public 
path, compatibility mode off. This is
      * the route most private REST calls take, and it consumes the Basic 
credential at its own call
-     * site — with only the {@code tenants} and V2 tests above, deleting the 
guard here would leave
+     * site — with only the {@code tenants} and compatibility-mode tests 
above, deleting the guard here would leave
      * the whole suite green.
      */
     @Test
@@ -159,7 +159,7 @@ class AuthenticationFilterBlankPasswordTest {
     }
 
     /**
-     * Control for the ordinary V3 branch: a non-blank credential must still 
be offered to the tenant
+     * Control for the ordinary multi-tenant branch: a non-blank credential 
must still be offered to the tenant
      * private-key check and then to JAAS.
      */
     @Test
@@ -190,7 +190,7 @@ class AuthenticationFilterBlankPasswordTest {
         assertUnauthorizedWithoutReachingJaas(requestContext);
     }
 
-    /** Control for the V2 branch: a non-blank credential must still reach 
JAAS there too. */
+    /** Control for the compatibility-mode branch: a non-blank credential must 
still reach JAAS there too. */
     @Test
     void 
filterPassesNonBlankPasswordToJaasOnAPrivatePathInV2CompatibilityMode() throws 
IOException {
         
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
diff --git 
a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy
 
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy
index 2cb53fdac..c9fcc575c 100644
--- 
a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy
+++ 
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy
@@ -88,7 +88,7 @@ context.performMigrationStep("4.0.0-create-tenant-index", () 
-> {
         String publicKeyId  = UUID.randomUUID().toString()
         String privateKeyId = UUID.randomUUID().toString()
 
-        // Only the salted hash and a masked, display-safe representation are 
ever persisted (see UNOMI-938).
+        // Only the SHA-256 hash and a masked, display-safe representation are 
ever persisted (see UNOMI-938).
         // The plaintext values below are only available now, at generation 
time.
         String publicKeyHash  = hashApiKey(generatedPublicKey)
         String privateKeyHash = hashApiKey(generatedPrivateKey)
@@ -115,8 +115,8 @@ PRIVATE KEY  (HTTP Basic authentication as 
tenantId:privateKey -- protected / ad
 
 IMPORTANT: These keys cannot be recovered after this file is deleted.
            Save them in a password manager or secrets vault now.
-           This file is automatically deleted 24 hours after creation
-           by the next migration run. Delete it manually once saved.
+           A later migration run deletes this file once it is more than
+           24 hours old. Delete it manually once the keys are saved.
 ${sep}
 """
         Files.write(keyFile, fileContent.getBytes("UTF-8"))
@@ -126,7 +126,7 @@ ${sep}
         context.printMessage("  Public key  : ${generatedPublicKey}")
         context.printMessage("  Private key : ${generatedPrivateKey}")
         context.printMessage("  Keys file   : ${keyFile}")
-        context.printMessage("  (auto-deleted after 24 h; delete manually once 
keys are saved)")
+        context.printMessage("  (delete it once the keys are saved; a later 
migration run removes it after 24 h)")
         context.printMessage(sep)
 
         // Create the default tenant document
@@ -135,7 +135,7 @@ ${sep}
             "itemType": "tenant",
             "name": "Default Tenant",
             "tenantId": "system",
-            "description": "Default tenant created during migration to Unomi 
V3",
+            "description": "Default tenant created during migration to Unomi 
4.0",
             "createdBy": "system-migration-4.0.0",
             "lastModifiedBy": "system-migration-4.0.0",
             "creationDate": "${isoDate}",

Reply via email to