Skip to content

Add bounded client app metadata admin messages#907

Open
gotnull wants to merge 1 commit into
meshtastic:masterfrom
gotnull:feature/client-app-data-admin
Open

Add bounded client app metadata admin messages#907
gotnull wants to merge 1 commit into
meshtastic:masterfrom
gotnull:feature/client-app-data-admin

Conversation

@gotnull
Copy link
Copy Markdown

@gotnull gotnull commented May 5, 2026

Summary

Add a small new wire surface so Meshtastic companion applications can ask their locally-connected node to persist a few opaque, app-defined metadata records on their behalf. The intent is to give companion apps an explicit place for non-secret convenience metadata, instead of overloading user-visible fields like long_name or unrelated configuration surfaces.

This is intentionally namespaced, not owned: the firmware enforces shape, payload size, and record-count limits, but it does NOT authenticate which client is writing. Any admin-capable client may overwrite or delete any app_id. Clients must therefore treat stored payloads as untrusted, optional, and recoverable.

Companion to the firmware implementation PR (meshtastic/firmware#10392) and the RFC (Meshtastic/rfcs#12).

Wire / API shape

New top-level message in meshtastic/admin.proto:

message ClientAppData {
  string  app_id     = 1;   // ^[a-z0-9._-]{1,32}$
  uint32  version    = 2;   // app-defined schema version; firmware does not interpret
  bytes   payload    = 3;   // opaque to firmware, max 512 bytes
  fixed32 updated_at = 4;   // unix epoch seconds, firmware-set on write, 0 if no valid time
}

Four new AdminMessage.payload_variant fields:

ClientAppData set_client_app_data            = 104;
string        get_client_app_data_request    = 105;
ClientAppData get_client_app_data_response   = 106;
string        delete_client_app_data_request = 107;

New on-disk wrapper in meshtastic/localonly.proto:

message LocalClientAppData {
  repeated ClientAppData records = 1;   // capped at 4 by localonly.options
  uint32   version               = 2;
}

nanopb sizing in the .options files:

*ClientAppData.app_id max_size:33
*ClientAppData.payload max_size:512
*AdminMessage.get_client_app_data_request max_size:33
*AdminMessage.delete_client_app_data_request max_size:33
*LocalClientAppData.records max_count:4

Limits

app_id regex ^[a-z0-9._-]{1,32}$
payload max 512 bytes
record max 4 records per node
updated_at firmware-set fixed32 unix epoch seconds, 0 when RTC is invalid

Worst-case on-disk footprint = meshtastic_LocalClientAppData_size = 2258 bytes.

Namespaced, not owned

The firmware does not authenticate which companion app is making a write request. app_id prevents accidental name collisions between apps; it does NOT prove caller ownership. Any admin-capable client may be able to write or delete any app_id.

Clients must treat stored metadata as untrusted, optional, and recoverable. The companion app's own local/cloud/export storage remains the source of truth. Do not store secrets, identity keys, session keys, paid-entitlement state, trust authority, blocklists, or any data used to make security, routing, authentication, or purchase decisions.

Records are local-only by construction:

  • never broadcast over LoRa
  • never included in NodeInfo
  • never relayed via MQTT
  • firmware never interprets payload
  • cleared by factory reset via the existing /prefs wipe path

Compatibility

Wire-additive only. Existing clients that don't know about the new tags are unaffected. Older firmware that doesn't yet implement the feature returns Routing_Error_NOT_AUTHORIZED or no reply for the new tags; companion apps must treat that as "feature unavailable, fall back to app-local storage."

No version bump. No renumbering. No breaking changes to existing types.

Validation

$ buf format --diff .
(no output, exit 0)

$ buf lint .
(no output, exit 0)

$ buf breaking . --against '.git#ref=origin/master'
(no output, exit 0)

All three buf checks pass clean against master. The firmware-side implementation regenerates nanopb output (committed in the firmware PR) and passes both bash bin/test-native-docker.sh -f test_default and bash bin/test-native-docker.sh -f test_client_app_data (27 store-level tests covering CRUD, slot accounting, payload bounds, persistence, factory-reset semantic, and storage-error injection).

Linked

Maintainer questions

Specifically requesting confirmation on:

  1. Field-number allocation 104..107. This sits in the next contiguous gap after sensor_config = 103. Happy to move if you have a different range in mind.
  2. Empty-app_id get-miss sentinel. A read of an unknown key returns a get_client_app_data_response whose app_id is empty. This avoids introducing a one-off response-envelope pattern for this single feature, but it is a minor novelty. Acceptable, or do you want to wait for a broader admin-wide status model?
  3. 512-byte / 4-record limits. Worst-case 2.3 KB persistent footprint. Is this acceptable across constrained targets (STM32WL, low-flash nRF52 variants)? Should there be a MESHTASTIC_EXCLUDE_CLIENT_APP_DATA compile-out flag?

Other open questions are listed in the RFC.

Add ClientAppData message and four AdminMessage oneof fields (104..107)
giving companion apps a small, bounded, local-node-only place to persist
opaque app-defined state. Pair with LocalClientAppData on-disk wrapper
in localonly.proto and matching nanopb sizing.

Wire surface (additive):
  message ClientAppData { string app_id = 1; uint32 version = 2;
                          bytes payload = 3; fixed32 updated_at = 4; }
  AdminMessage.payload_variant:
    ClientAppData set_client_app_data            = 104;
    string        get_client_app_data_request    = 105;
    ClientAppData get_client_app_data_response   = 106;
    string        delete_client_app_data_request = 107;

Limits: app_id matches ^[a-z0-9._-]{1,32}$, payload <= 512 bytes, max 4
records per node. updated_at is firmware-set on write (0 if no valid time).

Important: namespaced, not owned. Firmware enforces shape and capacity
but does NOT authenticate which client is writing. Any admin-capable
client may overwrite or delete any app_id. Clients must treat payloads
as untrusted, optional, and recoverable. Do not store secrets, identity
keys, paid-entitlement state, or any data used for security/routing/
authentication/purchase decisions.

Records are local-only by construction: never broadcast over LoRa, never
included in NodeInfo, never relayed via MQTT. Cleared by factory reset
via the existing /prefs wipe path.

buf format / lint / breaking against master all pass.

TODO(maintainer): confirm field-number allocation 104..107, record cap,
and whether remote authenticated admin should be allowed to read.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 5, 2026

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants