# How OpenLogi Handles Config Schema Migration for Bindings and Physical Device Keys

> Discover how OpenLogi automates config schema migration for bindings and physical device keys on load, ensuring data integrity with versioning and backups.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-26

---

**OpenLogi implements a load-time migration system in its core crate that automatically rewrites TOML configuration files on first read after an application upgrade, versioning both binding definitions and physical device keys while creating pre-migration backups to prevent data loss.**

The AprilNEA/OpenLogi repository stores user preferences in evolving TOML files that must remain compatible across application updates. To manage this, the project employs a sophisticated **config schema migration** pipeline that intercepts file loading, applies necessary transformations, and persists the upgraded format without manual user intervention.

## Version-Gated Migration Entry Point

All migration logic originates in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs), where the `Config` struct tracks the `schema_version` of each file. When `Config::load` deserializes a configuration file, it immediately invokes `Self::apply_migrations(version)` to determine which transformations are required.

This method iterates over all registered migrations whose target version is greater than the file's current version. Only after applying every necessary upgrade does the application use the configuration, ensuring that internal logic always operates on the latest schema regardless of the file's original format.

*Source:* [`crates/openlogi-core/src/config.rs#L354-L362`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config.rs#L354-L362)

## Migrating Binding Definitions Across Schema Versions

Bindings are versioned entities in OpenLogi because variant renames and structural changes constitute migration events. The system specifically handles owner-locked gesture bindings through a dedicated migration path.

### Handling Owner-Locked Gesture Bindings

For configuration files with schema versions ≤ 3, the system invokes `Config::migrate_owner_locked_gestures` to transform legacy gesture ownership models. This migration executes four critical steps:

- **Determining the gesture owner** – The migration inspects `device.gesture_owner.take()` and passes it to `Self::infer_gesture_owner` to establish which physical button dispatches gesture events when the field is missing from older configs.

- **Demoting non-owner gestures** – Every button that is not the owner but contains a `Binding::Gesture` is moved into `DeviceConfig::disabled_gestures` using `device.disabled_gestures.insert(*id, map.clone())`. The binding is then downgraded to a `Binding::Single` containing only the `Click` action via `binding.demote_to_single(...)`.

- **Materializing the owner's map** – If the owner is a HID++ gesture source, the migration seeds a full direction map containing all `GestureDirection` variants with default bindings. For non-gesture sources like haptic panels, the map is only inserted when explicitly needed.

- **Discarding deprecated fields** – After migration completes, the consumed `gesture_owner` field is never serialized again; the shape of the binding itself becomes the source of truth, as noted in the source comment `// the consumed gesture_owner never serializes again`.

*Source:* [`crates/openlogi-core/src/config.rs#L354-L410`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config.rs#L354-L410)

## Physical Device Key Migration

Device identification keys underwent significant changes between v1 and v2 of the config format. The migration shim located in [`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs) handles the transition by detecting legacy entry patterns.

The system identifies keys prefixed with `receiver:` or `raw:` and preserves those entries unchanged for devices that still rely on the old naming convention, such as a G502 reporting a specific raw HID++ ID. For newer devices, the migration renames keys to match the current schema while maintaining backward compatibility with legacy hardware definitions.

*Source:* [`crates/openlogi-core/src/config/device.rs#L2-L10`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config/device.rs#L2-L10)

## Backup Strategy for Key-Rewriting Migrations

Because migrations may rewrite the keys of an entire configuration file, OpenLogi implements a defensive backup mechanism. When a migration that alters file structure runs, `ConfigFile::migration_backup_path` generates a backup copy named `<original>_pre-migration_<version>.toml`.

This backup is written **before** the mutated configuration is saved, ensuring that a failure during the write operation cannot corrupt or lose the user's original file. The backup remains available for manual recovery if the migration produces unexpected results.

*Source:* [`crates/openlogi-core/src/config/file.rs#L104-L210`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config/file.rs#L104-L210)

## Testing Migration Correctness

The core crate includes a comprehensive test suite in [`crates/openlogi-core/src/config/tests.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/tests.rs) that exercises each migration path. These tests verify that backups are produced correctly, validate that resulting configurations match the expected modern schema, and ensure that data transformations preserve user-defined settings like disabled gestures across version upgrades.

*Source:* [[`crates/openlogi-core/src/config/tests.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/tests.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config/tests.rs)

## Code Examples

The following examples demonstrate how to interact with the migration system programmatically:

```rust
// Automatically migrate a legacy config file on load
let cfg = Config::load("config.toml")?; // applies migrations if needed
println!("Current schema version: {}", cfg.schema_version);

```

```rust
// Manual invocation of the gesture-owner migration
let mut cfg = Config::load("config.toml")?;
cfg.migrate_owner_locked_gestures(); // rewrites older gesture bindings
cfg.save()?;                         // persists the migrated file

```

```rust
// Inspect the backup created by a migration
let backup_path = migration_backup_path("config.toml", old_version)?;
println!("Backup of pre-migration config: {}", backup_path.display());

```

## Summary

- **Load-time detection**: `Config::load` in [`config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/config.rs) automatically detects file versions and triggers `apply_migrations` before returning the configuration object.
- **Binding evolution**: The `migrate_owner_locked_gestures` method handles complex binding restructures, including inferring gesture owners and demoting incompatible bindings to single actions.
- **Device key compatibility**: Legacy device identifiers with `receiver:` or `raw:` prefixes are preserved or renamed in [`device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/device.rs) based on hardware requirements.
- **Data safety**: The `migration_backup_path` mechanism creates timestamped backups before any destructive key-rewriting operations occur.
- **Verification**: Comprehensive tests in [`tests.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/tests.rs) ensure that every migration path produces valid modern schemas without data loss.

## Frequently Asked Questions

### How does OpenLogi determine which migrations to run?

OpenLogi stores a `schema_version` integer in every TOML file. When `Config::load` executes, it compares this version against the application's current schema version. The system then invokes `apply_migrations`, which iterates through all migration functions targeting versions newer than the file's current state, applying them sequentially until the configuration reaches the latest format.

### What happens to my original config file during a migration?

Before any changes are written, OpenLogi creates a backup using `ConfigFile::migration_backup_path`. This generates a file named `<original>_pre-migration_<version>.toml` containing the exact pre-migration state. Only after successfully creating this backup does the system write the upgraded configuration, ensuring you can manually restore the original if necessary.

### Why are binding variants considered migration events in OpenLogi?

Binding variants require migration handling because structural changes—such as the transition to owner-locked gesture bindings in schema v3—affect how input events are dispatched. When the configuration format changes to remove fields like `gesture_owner` in favor of binding-shape-based logic, the migration system must transform existing user configurations to match the new API without losing custom gesture mappings or disabled actions.

### Where are physical device key migrations implemented?

Physical device key migrations are handled in [`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs). This module specifically manages the transition between v1 and v2 configuration formats, detecting legacy keys that start with `receiver:` or `raw:` and determining whether to preserve them for backward compatibility or rename them according to modern device identification standards.