> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# C++ V4→V5 API Migration Guide

> Guide for migrating C++ apps from Ditto v4 to v5.

## Overview

This guide covers the essential changes needed to migrate your Ditto C++ app from v4 to v5. The main architectural shift is removing the deprecated identity-based constructors and requiring the four-phase configuration model (`DittoConfig` + `Ditto::open`) that was introduced in late v4. v5 also removes all legacy collection-based APIs in favor of DQL.

<Note>
  If you already use `Ditto::open(DittoConfig)` in v4, the initialization code is largely the same — focus on the removed APIs, renames, and DQL migration sections below.
</Note>

**Also required:** v5 uses DQL (Ditto Query Language) for all data operations. See the [DQL Migration Guide](./cpp-dql) for query migration steps.

***

## AI Agent Prompt

Use this prompt when working with an AI coding assistant to migrate your Ditto C++ app from v4 to v5.

<Accordion title="Copy AI Migration Prompt (Click to Expand)">
  ````text theme={null}
  I need help migrating a Ditto C++ application from v4 to v5. Focus on these critical changes:

  INITIALIZATION:
  Replace deprecated Ditto() constructor with Ditto::open(config) using builder pattern.
  - Create DittoConfig with set_database_id and set_connect
  - Identity::OnlinePlayground(app_id, token, cloud_sync, url) → DittoConfig::default_config().set_database_id(app_id).set_connect(DittoConfig::Connect::server(url))
  - Identity::OfflinePlayground(app_id) → DittoConfig::default_config().set_database_id(app_id).set_connect(DittoConfig::Connect::small_peers_only())
  - Identity::SharedKey(app_id, key) → DittoConfig::default_config().set_database_id(app_id).set_connect(DittoConfig::Connect::small_peers_only(key))
  - Identity::Manual(cert) → No v5 equivalent — consult Ditto support
  - Ditto::open(config) returns std::shared_ptr<Ditto>
  - Remove disable_sync_with_v3, DQL strict mode workarounds
  - IMPORTANT: If v4 app used default (strict=true), set DQL_STRICT_MODE=true BEFORE starting sync
  - Remove Ditto::close() calls — use shared_ptr lifetime management
  - Convenience shortcuts: set_server_connect(url) and set_small_peers_only_connect(key)
  - Running update_transport_config is no longer needed to update a websocket_urls when using a Big Peer as this is automatically set by the SDK from the auth server URL.  

  AUTHENTICATION:
  Set set_expiration_handler lambda after Ditto::open().
  - Handler signature: std::function<void(Ditto &ditto, uint32_t sec_remaining)>
  - Access auth via ditto.get_auth()-> (not ditto.auth())
  - get_auth() returns nullptr for non-authenticated configs — always null-check
  - Handler called when sec_remaining == 0 (initial auth) or > 0 (token expiring)
  - Use Authenticator::get_development_provider() for playground tokens
  - Login: ditto.get_auth()->login(token, provider, [](std::unique_ptr<std::string> client_info, std::unique_ptr<DittoError> error) { ... })
  - New: Authenticator::get_status(), observe_status(callback), logout(cleanup)

  SYNC:
  - ditto->start_sync() → ditto->get_sync().start()
  - ditto->stop_sync() → ditto->get_sync().stop()
  - ditto->get_is_sync_active() → ditto->get_sync().is_active()

  PRESENCE:
  - ditto->observe_peers(cb) → ditto->get_presence().observe(cb)
  - peer.peer_key_string → peer.peer_key
  - peer.is_connected_to_ditto_cloud → peer.is_connected_to_ditto_server
  - connection.peer_key_string1/2 → connection.peer1/2
  - connection.approximate_distance_in_meters → REMOVED, no replacement
  - New: Presence::graph(), peer_metadata(), set_peer_metadata(), set_connection_request_handler()

  COLLECTION API → DQL:
  All collection APIs are removed. Replace with get_store().execute() using DQL.
  - collection.find("color == 'blue'").exec() → ditto->get_store().execute("SELECT * FROM cars WHERE color = :color", {{"color", "blue"}})
  - collection.find_by_id(id).exec() → ditto->get_store().execute("SELECT * FROM cars WHERE _id = :id", {{"id", id}})
  - collection.upsert(value) → ditto->get_store().execute("INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", {{"doc", value}})
  - collection.find_by_id(id).update(...) → ditto->get_store().execute("UPDATE cars SET color = :color WHERE _id = :id", {{"color", "green"}, {"id", id}})
  - collection.find_by_id(id).remove() → ditto->get_store().execute("DELETE FROM cars WHERE _id = :id", {{"id", id}})
  - collection.find_by_id(id).evict() → ditto->get_store().execute("EVICT FROM cars WHERE _id = :id", {{"id", id}})
  - Always use parameterized queries with :paramName — NEVER use string concatenation

  WRITE TRANSACTIONS:
  - Store::write(fn) with WriteTransaction → REMOVED
  - Replace with Store::transaction(fn), execute_transaction(fn), or transaction_returning<T>(fn)
  - TransactionCompletionAction::commit / rollback controls outcome

  LIVE QUERIES → STORE OBSERVERS:
  - collection.find("...").observe_local([](auto docs, auto event) { ... }) → ditto->get_store().register_observer("SELECT ...", args, [](QueryResult result) { ... })
  - observer->cancel() to stop (same as v4)
  - v4 handler: (std::vector<Document>, LiveQueryEvent) → v5 handler: (QueryResult)

  SUBSCRIPTIONS:
  - collection.find("color == 'red'").subscribe() → ditto->get_sync().register_subscription("SELECT * FROM cars WHERE color = :color", {{"color", "red"}})

  BACK PRESSURE:
  - StoreObservationHandlerWithNextSignal → StoreObservationHandlerWithSignalNext
  - NextSignal → SignalNext (std::function<void()>)
  - Standard observer auto-signals; use SignalNext overload for manual control

  LOGGING:
  - Log:: → Logger:: (all static methods)
  - Log::Callback (raw function pointer) → Logger::Callback (std::function)
  - Log::set_custom_log_cb → Logger::set_custom_log_cb
  - Log::set_log_file(path) → REMOVED (continuous file logging gone; use Logger::set_custom_log_cb() for ongoing file output)
  - Log::disable_log_file() → REMOVED
  - Log::export_to_file(path) → Logger::export_to_file(path) (one-time dump, NOT a set_log_file replacement)
  - Log::set/get_emoji_log_level_headings_enabled → removed

  DISK USAGE:
  - DiskUsageChild → DiskUsageItem
  - DiskObserverContext → DiskUsageObserver
  - DiskUsage::exec() → DiskUsage::item()

  TRANSPORT CONFIG:
  - update_transport_config still works in v5
  - HttpListenConfig::static_content_path → REMOVED
  - All other fields unchanged

  DITTO CLASS:
  - Ditto() deprecated constructors → removed (use Ditto::open(config))
  - Ditto::close() → removed (use shared_ptr lifetime)
  - Ditto::get_sdk_version() → Ditto::get_version()
  - ditto_semver_version() → ditto::internal::ditto_semver_version() (use Ditto::get_version() instead)
  - Identity class hierarchy → removed (use DittoConfig)

  CONTAINERS:
  - store.observers (public field) → removed, use store.get_observers() (returns std::unordered_set)
  - Sync::subscriptions (public field) → removed, use Sync::get_subscriptions() (returns SyncSubscriptionSet = std::unordered_set)

  REMOVED APIs (no replacement):
  - Ditto::close() — shared_ptr lifetime management
  - Ditto::disable_sync_with_v3() — v3 protocol dropped
  - Log emoji headings — removed
  - Log::set_log_file/disable_log_file — continuous file logging removed (use Logger::set_custom_log_cb() for ongoing file output; Logger::export_to_file() is one-time only)
  - Identity class hierarchy — replaced by DittoConfig
  - Identity::Manual — no equivalent
  - LiveQueryEvent::hash()/hash_mnemonic() — query hashing removed
  - DocumentIdPath/AbstractDocumentPath — removed with legacy collection API
  - Connection::approximate_distance_in_meters — peer distance estimation removed
  - HttpListenConfig::static_content_path — static content serving removed
  - 38 legacy headers removed (Collection, Document, LiveQuery, WriteTransaction, etc.)

  BEFORE (V4):
  ```cpp
  auto ditto = Ditto(
      Identity::OnlinePlayground(
          "your-app-id",
          "your-token",
          false,
          "https://your-server.ditto.live"
      )
  );
  ditto.update_transport_config([](auto& config) {
      config.connect.websocket_urls.insert("wss://...");
  });
  ditto.get_store().execute("ALTER SYSTEM SET DQL_STRICT_MODE = false", {});
  ditto.disable_sync_with_v3();
  ditto.start_sync();
  ```

  AFTER (V5):
  ```cpp
  auto config = DittoConfig::default_config()
      .set_database_id("your-database-id")
      .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"));

  auto ditto = Ditto::open(config);

  ditto->get_auth()->set_expiration_handler([](Ditto &ditto, uint32_t sec_remaining) {
      ditto.get_auth()->login(
          "your-token",
          Authenticator::get_development_provider(),
          [](std::unique_ptr<std::string> client_info, std::unique_ptr<DittoError> error) {
              if (error) {
                  std::cout << "Authentication failed: " << error->what() << std::endl;
              }
          }
      );
  });

  ditto->get_sync().start();
  ```

  CHECKLIST:
  - Replace Ditto() deprecated constructor with Ditto::open(config)
  - Create DittoConfig with builder pattern
  - Set set_expiration_handler for authentication (ditto->get_auth()->)
  - Remove disable_sync_with_v3, DQL strict mode
  - Replace observe_peers(cb) with get_presence().observe(cb)
  - Replace peer_key_string with peer_key
  - Replace is_connected_to_ditto_cloud with is_connected_to_ditto_server
  - Replace connection.peer_key_string1/2 with connection.peer1/2
  - Remove approximate_distance_in_meters usage
  - Replace Log:: with Logger::
  - Replace StoreObservationHandlerWithNextSignal with StoreObservationHandlerWithSignalNext
  - Replace NextSignal with SignalNext
  - Replace start_sync() → get_sync().start(), stop_sync() → get_sync().stop()
  - Replace get_sdk_version() with get_version()
  - Remove Ditto::close() calls
  - Replace DiskUsageChild with DiskUsageItem, DiskUsage::exec() with item()
  - Replace Store::write() with Store::transaction() or execute_transaction()
  - Remove HttpListenConfig::static_content_path references

  Work through the codebase systematically. Show me each file's changes.
  ````
</Accordion>

***

## Ditto Instance Initialization

v5 removes the deprecated `Ditto()` constructors and the `Identity` class hierarchy. `Ditto::open(DittoConfig)` — which was already available in v4 — is now the only way to create a Ditto instance.

v5 separates initialization into four distinct phases for better clarity and control:

<CodeGroup>
  ```cpp C++ (v5) theme={null}
  // 1. Configure
  auto config = DittoConfig::default_config()
      .set_database_id("your-database-id")
      .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"));

  // 2. Initialize
  auto ditto = Ditto::open(config);

  // 3. Authenticate
  ditto->get_auth()->set_expiration_handler([](Ditto &ditto, uint32_t sec_remaining) {
      ditto.get_auth()->login(
          "your-token",
          Authenticator::get_development_provider(),
          [](std::unique_ptr<std::string> client_info, std::unique_ptr<DittoError> error) {
              if (error) {
                  std::cout << "Authentication failed: " << error->what() << std::endl;
              } else {
                  std::cout << "Authentication successful" << std::endl;
              }
          }
      );
  });

  // 4. Start sync
  ditto->get_sync().start();
  ```

  ```cpp C++ (v4 — deprecated constructor) theme={null}
  // Everything mixed together
  auto ditto = Ditto(
      Identity::OnlinePlayground(
          "your-app-id",
          "your-token",
          false,
          "https://your-server.ditto.live"
      )
  );

  ditto.update_transport_config([](auto& config) {
      config.connect.websocket_urls.insert("wss://...");
  });
  ditto.get_store().execute("ALTER SYSTEM SET DQL_STRICT_MODE = false", {});
  ditto.disable_sync_with_v3();

  ditto.start_sync();
  ```
</CodeGroup>

**These changes provide:**

* Clear separation of connection, initialization, and authentication concerns
* Builder pattern for flexible configuration
* No workarounds needed (DQL strict mode, v3 sync disable)
* Type-safe configuration with compile-time validation

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Initialization">
    Replace the deprecated `Ditto()` constructor with `Ditto::open(config)`.

    <CodeGroup>
      ```cpp C++ (v5) theme={null}
      auto config = DittoConfig::default_config()
          .set_database_id("your-database-id")
          .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"));

      auto ditto = Ditto::open(config);
      ditto->get_sync().start();
      ```

      ```cpp C++ (v4 — deprecated constructor) theme={null}
      auto ditto = Ditto(
          Identity::OnlinePlayground(
              "your-app-id",
              "your-token",
              false,
              "https://your-server.ditto.live"
          )
      );
      ditto.start_sync();
      ```
    </CodeGroup>

    **Key changes:**

    * Use `DittoConfig::default_config()` builder pattern instead of `Identity`
    * `app_id` → `database_id`
    * `Identity::OnlinePlayground` → `DittoConfig::Connect::server(...)`
    * `Identity::OfflinePlayground` → `DittoConfig::Connect::small_peers_only()` (empty string = no encryption)
    * `Identity::SharedKey` → `DittoConfig::Connect::small_peers_only("your-shared-key")`
    * Remove `disable_sync_with_v3()` calls
    * Convenience shortcuts available: `set_server_connect(url)` and `set_small_peers_only_connect(key)` can replace `set_connect(DittoConfig::Connect::server(url))`

    **Identity Type Mapping:**

    | v4 Identity                                                             | v5 Equivalent                                                                                                                                  |
    | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | `Identity::OnlinePlayground(app_id, token, cloud_sync, url)`            | `DittoConfig::Connect::server(url)` + `auth.set_expiration_handler(...)` + `auth.login(token, Authenticator::get_development_provider(), ...)` |
    | `Identity::OnlineWithAuthentication(app_id, callback, cloud_sync, url)` | `DittoConfig::Connect::server(url)` + `auth.set_expiration_handler(...)` + custom auth logic                                                   |
    | `Identity::OfflinePlayground(app_id)`                                   | `DittoConfig::Connect::small_peers_only()` + `ditto->set_offline_only_license_token(token)`                                                    |
    | `Identity::SharedKey(app_id, shared_key)`                               | `DittoConfig::Connect::small_peers_only(shared_key)`                                                                                           |
    | `Identity::Manual(certificate_config)`                                  | **Removed** — no direct equivalent, consult Ditto support                                                                                      |
  </Step>

  <Step title="Update Authentication">
    Set authentication handler separately after initialization.

    <CodeGroup>
      ```cpp C++ (v5) theme={null}
      // Set handler after Ditto::open()
      // Handler signature: void(Ditto &ditto, uint32_t sec_remaining)
      ditto->get_auth()->set_expiration_handler([](Ditto &ditto, uint32_t sec_remaining) {
              ditto.get_auth()->login(
                  "your-token",
                  Authenticator::get_development_provider(),
                  [](std::unique_ptr<std::string> client_info, std::unique_ptr<DittoError> error) {
                      if (error) {
                          std::cout << "Authentication failed: " << error->what() << std::endl;
                      } else {
                          std::cout << "Authentication successful" << std::endl;
                      }
                  }
              );
      });
      ```

      ```cpp C++ (v4 — deprecated constructor) theme={null}
      // Auth mixed with identity
      auto ditto = Ditto(
          Identity::OnlinePlayground(
              "your-app-id",
              "your-token",  // Token in identity
              false,
              "https://your-server.ditto.live"
          )
      );
      ```
    </CodeGroup>

    **Key changes:**

    * Authentication now uses `set_expiration_handler` lambda
    * Handler signature: `std::function<void(Ditto &ditto, uint32_t sec_remaining)>`
    * First param is `Ditto &` reference — access auth via `ditto.get_auth()->`
    * Handler called when auth required (`sec_remaining == 0`) or token expiring
    * Use `Authenticator::get_development_provider()` for playground tokens
    * Custom auth: pass provider name string directly to `login()`
    * `get_auth()` returns `nullptr` if not configured for authentication (e.g., `small_peers_only` without a server) — always null-check before accessing

    **Additional Authenticator APIs in v5:**

    * `Authenticator::get_status()` — returns current `AuthenticationStatus`
    * `Authenticator::observe_status(callback)` — observe status changes
    * `Authenticator::logout(cleanup)` — log out and optionally run cleanup
  </Step>
</Steps>

## Additional Changes

### DQL Strict Mode Behavior Change

<Warning>
  **Breaking Change**: v5 defaults to `DQL_STRICT_MODE=false`, which fundamentally changes how DQL queries behave.

  * **v4 default**: Objects treated as **REGISTER** (whole-object replacement)
  * **v5 default**: Objects treated as **MAP** (field-level merging)

  This affects the behavior of all DQL `SELECT`, `INSERT`, and `UPDATE` operations.
</Warning>

Choose the appropriate migration path based on your current v4 configuration:

<AccordionGroup>
  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=true (v4 default)">
    **If you're currently using the v4 default (`DQL_STRICT_MODE=true`)**, you **must** explicitly set strict mode to `true` in v5 before starting sync or executing any queries.

    <Warning>
      Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
    </Warning>

    <Note>
      To migrate to `DQL_STRICT_MODE=false` (the new v5 default), contact Ditto Customer Support for guidance.
    </Note>

    <CodeGroup>
      ```cpp C++ (v5 - Maintain v4 Behavior) highlight={5,6} theme={null}
      auto config = DittoConfig::default_config()
          .set_database_id("your-database-id")
          .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"));
      auto ditto = Ditto::open(config);

      // IMPORTANT: Set strict mode BEFORE starting sync or running queries
      ditto->get_store().execute("ALTER SYSTEM SET DQL_STRICT_MODE = true");

      // Now safe to start sync
      ditto->get_sync().start();
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=false">
    **If you explicitly set `DQL_STRICT_MODE=false` in v4**, no changes are required.

    v5 uses `DQL_STRICT_MODE=false` as the default, so your existing DQL queries will behave identically. You can upgrade freely.

    **Optional**: You can remove the explicit `ALTER SYSTEM SET DQL_STRICT_MODE = false` statement in v5 since this is now the default behavior.

    <CodeGroup>
      ```cpp C++ (v5) theme={null}
      auto config = DittoConfig::default_config()
          .set_database_id("your-database-id")
          .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"));
      auto ditto = Ditto::open(config);

      // No need to set DQL_STRICT_MODE - false is now the default
      ditto->get_sync().start();
      ```

      ```cpp C++ (v4 - Your Current Setup) theme={null}
      auto ditto = Ditto(
          Identity::OnlinePlayground(
              "your-app-id",
              "your-token",
              false,
              "https://your-server.ditto.live"
          )
      );

      // You explicitly set this in v4
      ditto.get_store().execute("ALTER SYSTEM SET DQL_STRICT_MODE = false", {});

      ditto.start_sync();
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using Legacy Query Builder">
    The Legacy Query Builder has been removed in v5. All queries must be converted to DQL before upgrading.

    **Good news**: Legacy Query Builder functionality has 1:1 support with `DQL_STRICT_MODE=false` (the v5 default), making migrations straightforward.

    **Migration Steps**:

    1. **In v4: Set `DQL_STRICT_MODE=false`** after initialization:
           <CodeGroup>
             ```cpp C++ (v4 - Enable Non-Strict Mode) theme={null}
             auto ditto = Ditto(
                 Identity::OnlinePlayground(
                     "your-app-id",
                     "your-token",
                     false,
                     "https://your-server.ditto.live"
                 )
             );

             // Set DQL_STRICT_MODE to false for Query Builder compatibility
             ditto.get_store().execute("ALTER SYSTEM SET DQL_STRICT_MODE = false", {});

             ditto.start_sync();
             ```
           </CodeGroup>

    2. **Convert all queries from Legacy Query Builder to DQL**
       See the [C++ Legacy→DQL Migration Guide](./cpp-dql) for detailed conversion examples.

    3. **Upgrade to v5**
       No DQL configuration changes required—v5 defaults to `DQL_STRICT_MODE=false`.
  </Accordion>
</AccordionGroup>

For additional guidance or questions, contact Ditto Customer Support.

### Default Persistence Directory

v5 includes the database ID in the default directory name: `ditto-{database_id}` instead of `ditto`. This is for only new databases created in v5. v5 does not migrate existing databases to the new structure.

**To maintain v4 compatibility:**

```cpp theme={null}
auto config = DittoConfig::default_config()
    .set_database_id("your-database-id")
    .set_connect(DittoConfig::Connect::server("https://your-server.ditto.live"))
    .set_persistence_directory("/app/data/ditto");  // Old v4 path
```

### Observer Changes

v4 and v5 both use callback-based observers. The core `register_observer` API pattern is the same in both versions, returning a `StoreObserver` that you hold via `shared_ptr` and call `.cancel()` on for cleanup.

<CodeGroup>
  ```cpp C++ (v5) theme={null}
  // Standard observer — signal_next called automatically after handler returns
  auto observer = ditto->get_store().register_observer(
      "SELECT * FROM cars WHERE color = :color",
      {{"color", "blue"}},
      [](QueryResult result) {
          process_results(result);
          // signal_next called automatically here
      }
  );

  // Cleanup: cancel explicitly or let shared_ptr go out of scope
  observer->cancel();
  ```

  ```cpp C++ (v4) theme={null}
  // Same pattern — register_observer existed in v4
  auto observer = ditto->get_store().register_observer(
      "SELECT * FROM cars WHERE color = :color",
      {{"color", "blue"}},
      [](QueryResult result) {
          process_results(result);
      }
  );

  // Cleanup
  observer->cancel();
  ```
</CodeGroup>

**Key changes:**

* v5 adds introspection methods to `StoreObserver`: `get_query()`, `get_query_arguments()`, `get_query_arguments_cbor_data()`, `get_query_arguments_json_string()`
* Container type changed: `store.get_observers()` returns `std::unordered_set` (v4 returned `std::set`)
* The deprecated `store.observers` public field is removed — use `store.get_observers()`

### Back Pressure (SignalNext)

The standard `register_observer` automatically calls `signal_next` after your handler returns. Use the `StoreObservationHandlerWithSignalNext` overload when your handler needs manual control over when the next callback is delivered.

<Note>
  **Coalescing behavior**: While your handler is processing, Ditto coalesces any intermediate changes. When you call `signal_next()`, you receive a single callback with the latest state — not every intermediate change.
</Note>

```cpp theme={null}
// Standard observer — signal_next called automatically after handler returns
auto observer = ditto->get_store().register_observer(
    "SELECT * FROM cars WHERE color = :color",
    {{"color", "blue"}},
    [](QueryResult result) {
        process_results(result);
        // signal_next called automatically here
    }
);

// Back-pressure observer — you control when the next callback fires
StoreObservationHandlerWithSignalNext handler = [](QueryResult result, SignalNext signal_next) {
    process_results(result);
    // ... do expensive work ...
    signal_next();  // Ready for next update — must call this
};

auto observer = ditto->get_store().register_observer(
    "SELECT * FROM cars WHERE color = :color",
    {{"color", "blue"}},
    handler
);
```

**Types:**

* `SignalNext` = `std::function<void()>` (renamed from v4's `NextSignal`)
* `StoreObservationHandlerWithSignalNext` = `std::function<void(QueryResult, SignalNext)>` (renamed from v4's `StoreObservationHandlerWithNextSignal`)

### Presence API Changes

Presence is now accessed through `ditto->get_presence()` rather than directly on the `Ditto` object.

<CodeGroup>
  ```cpp C++ (v5) theme={null}
  // Observe presence changes
  auto observer = ditto->get_presence().observe([](const PresenceGraph &graph) {
      for (const auto &peer : graph.remote_peers) {
          std::cout << "Peer: " << peer.peer_key << std::endl;
      }
  });

  // Get current presence graph
  auto graph = ditto->get_presence().graph();

  // Set peer metadata
  ditto->get_presence().set_peer_metadata({{"role", "sensor"}});
  ```

  ```cpp C++ (v4) theme={null}
  // Observe peers directly on Ditto
  auto observer = ditto.observe_peers([](const PresenceGraph &graph) {
      for (const auto &peer : graph.remote_peers) {
          std::cout << "Peer: " << peer.peer_key_string << std::endl;
      }
  });
  ```
</CodeGroup>

**Peer property renames:**

| v4.14                                       | v5.0                                |
| ------------------------------------------- | ----------------------------------- |
| `peer.peer_key_string`                      | `peer.peer_key`                     |
| `peer.is_connected_to_ditto_cloud`          | `peer.is_connected_to_ditto_server` |
| `connection.peer_key_string1`               | `connection.peer1`                  |
| `connection.peer_key_string2`               | `connection.peer2`                  |
| `connection.approximate_distance_in_meters` | **REMOVED — no replacement**        |

**New Presence APIs in v5:**

* `Presence::graph()` — get current `PresenceGraph` immediately
* `Presence::peer_metadata()` / `set_peer_metadata(json)` — get/set arbitrary peer metadata
* `Presence::peer_metadata_json_string()` / `set_peer_metadata_json_string(str)` — JSON string variants
* `Presence::set_connection_request_handler(handler)` — control which peers can connect

### DiskUsage API Changes

| v4.14                       | v5.0                                                     |
| --------------------------- | -------------------------------------------------------- |
| `DiskUsageChild` class      | `DiskUsageItem` class                                    |
| `DiskObserverContext` class | `DiskUsageObserver` class                                |
| `DiskUsage::exec()`         | `DiskUsage::item()`                                      |
| `ditto->get_disk_usage()`   | `ditto->get_disk_usage()` (unchanged)                    |
| `disk_usage.observe(cb)`    | `disk_usage.observe(cb)` (unchanged, callback type same) |

### Transport Config Changes

`update_transport_config` and `set_transport_config` remain available in v5. The key change is:

| v4.14                                   | v5.0        |
| --------------------------------------- | ----------- |
| `HttpListenConfig::static_content_path` | **REMOVED** |
| All other TransportConfig fields        | Unchanged   |

```cpp theme={null}
// Transport config still works in v5
ditto->update_transport_config([](TransportConfig &config) {
    config.peer_to_peer.bluetooth_le.enabled = true;
    config.peer_to_peer.lan.enabled = true;
});
```

### Transaction API Changes

v5 removes the deprecated `WriteTransaction` and related classes. Use the DQL-based transaction API instead:

<CodeGroup>
  ```cpp C++ (v5) theme={null}
  #include <Transaction.hpp>

  // Basic transaction
  auto result = ditto->get_store().transaction(
      [](Transaction &txn) -> TransactionCompletionAction {
          txn.execute("INSERT INTO cars DOCUMENTS (:doc)", {{"doc", car_data}});
          txn.execute("UPDATE inventory SET count = count - 1 WHERE _id = :id", {{"id", inv_id}});
          return TransactionCompletionAction::commit;
      }
  );

  // Convenience: void-returning lambda (auto-commits on return, rolls back on throw)
  ditto->get_store().execute_transaction([](Transaction &txn) {
      txn.execute("INSERT INTO cars DOCUMENTS (:doc)", {{"doc", car_data}});
  });

  // Return a value from a transaction
  auto car_id = ditto->get_store().transaction_returning<std::string>(
      [](Transaction &txn) -> std::string {
          auto result = txn.execute("INSERT INTO cars DOCUMENTS (:doc)", {{"doc", car_data}});
          return result.mutated_document_ids()[0].dump();
      }
  );
  ```

  ```cpp C++ (v4 — deprecated Write Transaction) theme={null}
  // Deprecated write transaction — REMOVED in v5
  ditto.get_store().write([](WriteTransaction &txn) {
      txn["cars"].upsert(car_data);
  });
  ```
</CodeGroup>

**Key changes:**

* `Store::write(fn)` with `WriteTransaction` — **removed**
* `WriteTransaction`, `ScopedWriteTransaction`, `WriteTransactionResult`, `UpdateResult` — **removed**
* DQL-based `Store::transaction()` and `Store::execute_transaction()` are the replacement
* New: `Store::transaction_returning<T>()` — return a value from a transaction
* `TransactionOptions` available: `set_read_only(bool)`, `set_hint(string)`

### API Renames

| v4.14                                           | v5.0                                                                                                                                                                                        |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ditto->start_sync()`                           | `ditto->get_sync().start()`                                                                                                                                                                 |
| `ditto->stop_sync()`                            | `ditto->get_sync().stop()`                                                                                                                                                                  |
| `ditto->get_is_sync_active()`                   | `ditto->get_sync().is_active()`                                                                                                                                                             |
| `ditto->observe_peers(cb)`                      | `ditto->get_presence().observe(cb)`                                                                                                                                                         |
| `Ditto::get_sdk_version()`                      | `Ditto::get_version()`                                                                                                                                                                      |
| `Log` class (all static methods)                | `Logger` class                                                                                                                                                                              |
| `Log::Callback` (raw function pointer)          | `Logger::Callback` (`std::function<void(LogLevel, const std::string&)>`)                                                                                                                    |
| `Log::set_custom_log_cb(callback)`              | `Logger::set_custom_log_cb(callback)`                                                                                                                                                       |
| `Log::set_log_file(path)`                       | **Removed** — continuous file logging has no v5 equivalent. Use `Logger::set_custom_log_cb()` to implement your own file writer, or call `Logger::export_to_file()` for one-time log dumps. |
| `Log::disable_log_file()`                       | **Removed** with `set_log_file`                                                                                                                                                             |
| `StoreObservationHandlerWithNextSignal`         | `StoreObservationHandlerWithSignalNext`                                                                                                                                                     |
| `NextSignal` type alias                         | `SignalNext` type alias                                                                                                                                                                     |
| `store.observers` (deprecated public field)     | Removed — use `store.get_observers()` (returns `std::unordered_set`)                                                                                                                        |
| `Sync::subscriptions` (deprecated public field) | Removed — use `Sync::get_subscriptions()` (returns `SyncSubscriptionSet`)                                                                                                                   |
| `store.get_observers()` returns `std::set`      | `store.get_observers()` returns `std::unordered_set`                                                                                                                                        |
| `Sync::get_subscriptions()` returns `std::set`  | `Sync::get_subscriptions()` returns `SyncSubscriptionSet` (unordered\_set)                                                                                                                  |
| `Ditto()` deprecated constructor                | Removed — use `Ditto::open(DittoConfig)`                                                                                                                                                    |
| `Identity` class hierarchy                      | Removed — use `DittoConfig`                                                                                                                                                                 |
| `DiskUsageChild`                                | `DiskUsageItem`                                                                                                                                                                             |
| `DiskObserverContext`                           | `DiskUsageObserver`                                                                                                                                                                         |
| `DiskUsage::exec()`                             | `DiskUsage::item()`                                                                                                                                                                         |

### Log → Logger Rename

All `Log::` call sites must be updated to `Logger::`:

```cpp theme={null}
// v4
Log::set_minimum_log_level(LogLevel::Debug);
Log::set_custom_log_cb([](LogLevel level, std::string msg) {
    std::cout << msg << std::endl;
});

// v5
Logger::set_minimum_log_level(LogLevel::Debug);
Logger::set_custom_log_cb([](LogLevel level, const std::string& msg) {
    std::cout << msg << std::endl;
});
```

Note: `Log::Callback` changed from a raw function pointer (`void (*)(LogLevel, std::string)`) to `std::function<void(LogLevel, const std::string&)>` — lambdas with captures are now supported.

`Logger::export_to_file(path)` performs a **one-time export** of accumulated logs to disk. It returns `std::future<uint64_t>` (number of bytes written). The output is gzip-compressed JSON lines (`.jsonl.gz` recommended extension). This is **not** a replacement for `Log::set_log_file()` — it existed in v4 as `Log::export_to_file()` and is only renamed in v5.

**Migrating from `Log::set_log_file()`**: Continuous real-time file logging has been removed in v5. If you need ongoing file logging, use `Logger::set_custom_log_cb()` to register a callback that writes to a file yourself.

### Header Count Reduced

v5.0 ships 50 header files (down from 86 in v4.14). 38 headers were removed (legacy collection, document, live query, write transaction, identity, and log APIs). 2 headers were added (`Logger.hpp`, `fnv_1a_hash.hpp`). If your build system includes specific headers by path, audit the include list after upgrading.

### Ditto Lifetime Management

`Ditto::close()` is removed. Manage `Ditto` lifetime via the `shared_ptr` returned by `Ditto::open()`. The instance is destroyed when all `shared_ptr` copies go out of scope.

***

## APIs Removed Without Replacement

| Removed API                                                                              | Notes                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Ditto::close()`                                                                         | Use `shared_ptr` lifetime management                                                                                                                                  |
| `Ditto::disable_sync_with_v3()`                                                          | v3 protocol support fully dropped                                                                                                                                     |
| `Ditto()` constructors (with `Identity`)                                                 | Use `Ditto::open(DittoConfig)`                                                                                                                                        |
| `Identity` class hierarchy                                                               | Replaced by `DittoConfig`                                                                                                                                             |
| `Identity::Manual`                                                                       | No v5 equivalent — consult Ditto support                                                                                                                              |
| `Log` class                                                                              | Replaced by `Logger` class                                                                                                                                            |
| `Log::get_emoji_log_level_headings_enabled()` / `set_emoji_log_level_headings_enabled()` | Emoji log headings removed                                                                                                                                            |
| `Log::set_log_file(path)` / `disable_log_file()`                                         | Continuous file logging removed — no direct replacement. Use `Logger::set_custom_log_cb()` for ongoing file output, or `Logger::export_to_file()` for one-time dumps. |
| `HttpListenConfig::static_content_path`                                                  | Static HTTP content serving removed                                                                                                                                   |
| `LiveQueryEvent::hash()` / `hash_mnemonic()`                                             | Query hash methods removed                                                                                                                                            |
| `Connection::approximate_distance_in_meters`                                             | Peer distance estimation removed                                                                                                                                      |
| `store.observers` (public field)                                                         | Use `store.get_observers()` method                                                                                                                                    |
| `Sync::subscriptions` (public field)                                                     | Use `Sync::get_subscriptions()` method                                                                                                                                |

### Legacy Collection API — Completely Removed

All collection-based query APIs (38 headers removed) are gone in v5. Use DQL queries via `get_store().execute()`.

| Removed Header / Class                                  | Replacement                                            |
| ------------------------------------------------------- | ------------------------------------------------------ |
| `Collection`                                            | DQL queries via `store.execute()`                      |
| `Document` / `MutableDocument`                          | `QueryResult` items                                    |
| `DocumentPath` / `MutableDocumentPath`                  | Access values via `QueryResult`                        |
| `DocumentIdPath` / `AbstractDocumentPath`               | Use `_id` field in DQL                                 |
| `PendingCursorOperation` / `PendingIDSpecificOperation` | DQL queries                                            |
| `PendingCollectionsOperation`                           | DQL: `SELECT DISTINCT collection FROM __collections`   |
| `LiveQuery` / `LiveQueryEvent` / `LiveQueryMove`        | `store.register_observer()`                            |
| `SingleDocumentLiveQueryEvent`                          | `store.register_observer()` with `WHERE _id = :id`     |
| `Subscription` (legacy)                                 | `sync.register_subscription()`                         |
| `WriteTransaction` / `ScopedWriteTransaction`           | `store.transaction()` or `store.execute_transaction()` |
| `WriteTransactionResult` / `UpdateResult`               | `QueryResult.mutated_document_ids()`                   |
| `WriteStrategy`                                         | DQL `ON ID CONFLICT` clauses                           |
| `SortDirection`                                         | DQL `ORDER BY ... ASC/DESC`                            |
| `Counter` / `MutableCounter`                            | DQL `PN_INCREMENT`                                     |
| `Register` / `MutableRegister`                          | DQL `SET`                                              |
| `CollectionsEvent`                                      | DQL queries                                            |

***

## New APIs in v5

### Ditto

* **`Ditto::get_version()`** — replaces `get_sdk_version()`
* **`Ditto::get_presence()`** — returns `Presence &` for presence operations

### Logger

* **`Logger` class** — replaces `Log`; callback type is now `std::function`
* **`Logger::export_to_file(path)`** — one-time export of accumulated logs; returns `std::future<uint64_t>` (bytes written as gzip-compressed JSON lines). Renamed from `Log::export_to_file()` — not a replacement for the removed `Log::set_log_file()`.

### Sync

* **`ditto->get_sync().start()` / `.stop()` / `.is_active()`** — sync lifecycle on `Sync` object
* **`SyncSubscriptionHash`** / **`SyncSubscriptionEq`** — hash/eq helpers for subscription `unordered_set`
* **`SyncSubscriptionSet`** type alias — `unordered_set` of `SyncSubscription` shared\_ptrs
* **`SyncSubscription::get_query()`** / **`get_query_arguments()`** / **`get_query_arguments_cbor_data()`** / **`get_query_arguments_json_string()`** — introspect registered subscription

### Store & Observers

* **`StoreObservationHandlerWithSignalNext`** / **`SignalNext`** — renamed from `NextSignal` variants
* **`StoreObserverHash`** / **`StoreObserverEq`** — hash/eq helpers for observer `unordered_set`
* **`StoreObserver::get_query()`** / **`get_query_arguments()`** / **`get_query_arguments_cbor_data()`** / **`get_query_arguments_json_string()`** — introspect registered observer
* **`Store::execute_transaction()`** — convenience transaction with auto-commit
* **`Store::transaction_returning<T>()`** — transaction that returns a value

### Authentication

* **`Authenticator::get_status()`** — returns current `AuthenticationStatus`
* **`Authenticator::observe_status(callback)`** — observe authentication status changes
* **`Authenticator::logout(cleanup)`** — log out with optional cleanup callback

### Presence

* **`Presence::observe(cb)`** — replaces `Ditto::observe_peers()`
* **`Presence::graph()`** — get current `PresenceGraph` immediately
* **`Presence::peer_metadata()`** / **`set_peer_metadata(json)`** — get/set arbitrary peer metadata
* **`Presence::peer_metadata_json_string()`** / **`set_peer_metadata_json_string(str)`** — JSON string variants
* **`Presence::set_connection_request_handler(handler)`** — control incoming peer connections

### DiskUsage

* **`DiskUsageItem`** — renamed from `DiskUsageChild`
* **`DiskUsageObserver`** — renamed from `DiskObserverContext`
* **`DiskUsage::item()`** — renamed from `DiskUsage::exec()`

### Configuration

* **`DittoConfig::set_server_connect(url)`** — convenience shortcut for `set_connect(Connect::server(url))`
* **`DittoConfig::set_small_peers_only_connect(key)`** — convenience shortcut for `set_connect(Connect::small_peers_only(key))`

***

## Migration Checklist

### Initialization

* [ ] Replace deprecated `Ditto()` constructor with `Ditto::open(config)`
* [ ] Create `DittoConfig` with `set_database_id` and `set_connect`
* [ ] Update `Identity::OnlinePlayground` → `DittoConfig::Connect::server(...)`
* [ ] Update `Identity::OfflinePlayground` → `DittoConfig::Connect::small_peers_only()`
* [ ] Update `Identity::SharedKey` → `DittoConfig::Connect::small_peers_only("your-shared-key")`
* [ ] Remove `disable_sync_with_v3()` calls
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE starting sync if maintaining v4 behavior**

### Authentication

* [ ] Set `set_expiration_handler` lambda after initialization
* [ ] Handler receives `Ditto &ditto, uint32_t sec_remaining`
* [ ] Access auth inside handler via `ditto.get_auth()->login(...)`
* [ ] Use `Authenticator::get_development_provider()` for playground tokens
* [ ] Remove authentication from identity configuration
* [ ] Null-check `get_auth()` — returns `nullptr` for non-authenticated configs

### Sync

* [ ] Update `start_sync()` → `get_sync().start()`
* [ ] Update `stop_sync()` → `get_sync().stop()`
* [ ] Update `get_is_sync_active()` → `get_sync().is_active()`

### Data Operations

* [ ] Replace all collection-based queries with DQL via `get_store().execute()`
* [ ] Use parameterized queries with `:paramName` — never string interpolation
* [ ] Replace `.upsert()` with `INSERT INTO ... ON ID CONFLICT DO UPDATE`
* [ ] Replace `.update {}` closures with `UPDATE SET` DQL
* [ ] Replace `.remove()` with `DELETE FROM`
* [ ] Replace `.evict()` with `EVICT FROM`
* [ ] Replace `.observe_local()` with `get_store().register_observer()`
* [ ] Replace `.subscribe()` with `get_sync().register_subscription()`
* [ ] Replace `Store::write(fn)` with `Store::transaction()` or `Store::execute_transaction()`

### Presence

* [ ] Update `observe_peers(cb)` → `get_presence().observe(cb)`
* [ ] Update `peer_key_string` → `peer_key`
* [ ] Update `peer_key_string1/2` → `peer1/2`
* [ ] Update `is_connected_to_ditto_cloud` → `is_connected_to_ditto_server`
* [ ] Remove `approximate_distance_in_meters` usage (no replacement)

### Logging

* [ ] Replace `Log::` with `Logger::` throughout codebase
* [ ] Update `Log::Callback` stored variables: raw function pointer → `std::function`
* [ ] Update `Log::set_custom_log_cb` → `Logger::set_custom_log_cb`
* [ ] Remove `Log::set_log_file(path)` calls — continuous file logging removed. Use `Logger::set_custom_log_cb()` if you need ongoing file output.
* [ ] Remove `Log::disable_log_file()` calls
* [ ] Remove `Log::set_emoji_log_level_headings_enabled()` calls

### DiskUsage

* [ ] Update `DiskUsageChild` → `DiskUsageItem`
* [ ] Update `DiskUsage::exec()` → `DiskUsage::item()`

### Breaking Changes

* [ ] Set `set_persistence_directory` if maintaining v4 directory structure
* [ ] Replace `StoreObservationHandlerWithNextSignal` → `StoreObservationHandlerWithSignalNext`
* [ ] Replace `NextSignal` → `SignalNext` type alias
* [ ] Replace `store.observers` field access with `store.get_observers()` (now returns `unordered_set`)
* [ ] Replace `Sync::subscriptions` field access with `Sync::get_subscriptions()` (now returns `unordered_set`)
* [ ] Remove `Ditto::close()` calls — manage lifetime via `shared_ptr`
* [ ] Replace `get_sdk_version()` with `get_version()`
* [ ] Audit header includes — header count reduced from 86 to 50 (38 removed, 2 added)
* [ ] If processing is expensive, use `StoreObservationHandlerWithSignalNext` overload for back-pressure control
* [ ] Remove `HttpListenConfig::static_content_path` references

### Verification

* [ ] Build compiles with zero errors
* [ ] No deprecated API warnings
* [ ] Observers update data immediately
* [ ] Authentication works before sync starts
* [ ] No memory leaks

***

## Common Pitfalls

1. **DQL\_STRICT\_MODE silent change**: Not setting `DQL_STRICT_MODE=true` when your v4 app used the default. Objects that were replaced whole will now merge at field level — causes unexpected data merging.

2. **Wrong auth accessor**: Using `ditto->auth()` instead of `ditto->get_auth()`. Inside the expiration handler, the `Ditto &` reference requires `ditto.get_auth()->login(...)` (note: `.` for the reference, `->` for the shared\_ptr).

3. **Null auth pointer**: `get_auth()` returns `nullptr` for `small_peers_only` configurations without a server. Always null-check before calling methods on the result.

4. **String interpolation in queries**: Never build queries with string concatenation. Always use parameterized queries: `execute("SELECT * FROM cars WHERE color = :color", {{"color", "blue"}})`.

5. **Counter initialization**: Don't put `0` in insert documents for counter fields. Counters are created on first `PN_INCREMENT`. Inserting `0` creates a REGISTER, not a COUNTER.

6. **Missing `ON ID CONFLICT`**: INSERT fails if document `_id` already exists without a conflict clause.

7. **`Log::Callback` type change**: v4 used a raw function pointer (`void(*)(LogLevel, std::string)`). v5 uses `std::function<void(LogLevel, const std::string&)>`. Code storing callbacks as function pointers must be updated.

8. **`shared_ptr` lifetime**: `Ditto::open()` returns `shared_ptr<Ditto>`. The instance is destroyed when all copies go out of scope. Do not call `close()` — it was removed.

9. **Observer container type**: `store.get_observers()` now returns `std::unordered_set` instead of `std::set`. Code that depends on ordered iteration must be updated.
