Persistence¶
A node keeps its state in SQLite. The host opens the database, picks the driver, and owns the handle. The module embeds the DDL its queries are written against, applies it, and never opens a database itself. This page lists what is stored where, what survives a restart, and how schema changes reach a host.
Opening the database¶
db, err := sql.Open("sqlite", "file:node.db?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)")
foreign_keys(1)is required. Removing a fabric deletes its rows throughON DELETE CASCADE, and SQLite ignores foreign keys silently with the pragma off.busy_timeoutmakes a contended endpoint-number allocation wait instead of failing.- Both pragmas go in the DSN. A pragma set with a statement applies to one pooled connection only.
node.New applies the device schemas (store.Apply and
sqlitestore.Apply) to Config.Store.DB on every boot. Both scripts are
idempotent.
Source: examples/reference-bridge/persist.go:24-60, store/doc.go:8-36, node/node.go:300-317.
The device database¶
Tables of store (the matter_* schema)¶
| Table | Since | Holds |
|---|---|---|
matter_fabrics |
v1 (v0.1.0) | each fabric: index, fabric id, node id, root certificate and key, vendor, label, compressed fabric id |
matter_node_identities |
v1 | per fabric: NOC, ICAC, the private key, the IPK as the commissioner sent it |
matter_group_keys |
v1 | group key sets per fabric, up to three epoch keys |
matter_group_key_map |
v1, rebuilt in v3 | GroupKeyMap: group id to key set; since v3 without a foreign key, so an entry may name a key set not yet written |
matter_acl_entries |
v1 | the AccessControl list per fabric, in order |
matter_resumption |
v1 | CASE resumption records with the peer's CATs |
matter_diagnostics |
v1 | a single-row counter table with a typed API; the facade keeps its counters in matter_settings instead |
matter_metadata |
v1 | integer counters: next_fabric_index, event_number_ceiling |
matter_settings |
v1 | string key/value pairs (next section) |
matter_persistent_subscriptions |
v1 | unused since v0.3.0; kept so old and new databases have the same schema |
matter_group_table |
v2 (v0.2.0) | GroupTable: the endpoints of each group |
matter_groupcast_groups |
v2 | the Groupcast cluster's per-group properties |
matter_server_subscriptions |
v2 | the subscriptions of active CASE sessions, to re-establish after a restart |
matter_ota_default_providers |
v4 (v0.3.0) | the OTA requestor's DefaultOTAProviders |
Tables of endpoint/sqlitestore¶
| Table | Holds |
|---|---|
matter_endpoints |
one row per source: its stable key's string form, its scope, its endpoint number and device type |
matter_endpoint_allocation |
the high-water mark of endpoint numbers. A number is never reused, because a controller caches accessories by number |
The stable key's string form is the primary key. If a host changes how its key renders, the row is orphaned and the source gets a new endpoint number.
Source: store/schema.sql:18-280, store/migrations.go:43-54, store/migrations/v1.up.sql:4-6, store/metadata.go:15, store/settings.go:50-55, endpoint/sqlitestore/schema.sql:13-60.
Settings keys¶
matter_settings holds the controller-written attribute values and the
facade's own state.
| Key | Written by | Holds |
|---|---|---|
basic_information.node_label, basic_information.location |
the facade | the root's NodeLabel and Location as a controller wrote them |
basic_information.configuration_version |
the facade | the root's ConfigurationVersion |
basic_information.unique_id |
the facade | UniqueID, derived on the first boot and pinned |
basic_information.unique_id_rotated |
(deprecated) | read once for a migration and cleared |
bridged_node_label.<key> |
the facade | a bridged endpoint's NodeLabel |
bridged_configuration_version.<key> |
the facade | a bridged endpoint's ConfigurationVersion |
scenes.<key> |
the facade | a bridged endpoint's scene table |
binding.<key>, binding.@<endpoint> |
the facade | an endpoint's Binding list; @ and the number for an endpoint without a stable key |
user_label.root, user_label.aggregator |
the facade | the UserLabel lists of the root and the Aggregator |
gendiag.reboot_count |
the facade | RebootCount: 0 on a database's first boot, then one more per boot |
gendiag.operational_ms |
the facade | accumulated operational time (gendiag.operational_hours is read once as its legacy form) |
ota_requestor.state |
store |
the OTA requestor's state |
access_control.extension.<fabric> |
cluster/core |
AccessControl's Extension list per fabric |
matter.fabric.<fabric>.vid.statement, .vvsc |
cluster/core |
the VID verification statement and VVSC per fabric |
<key> is the endpoint's stable key in String() form, the same identity
endpoint numbers are kept under. A host that keeps scenes and labels
itself sets Config.EndpointPersistence, and the scenes. and
bridged_node_label. keys are then not written.
A host may use the settings table for its own state, as the reference
daemon does with blind.position. Pick a prefix that none of the keys
above use.
Source: store/settings.go:13-48, node/persist.go:22-36, node/persist.go:55-75, node/persist.go:250-305, node/persist.go:323-332, node/root.go:39-40, store/ota.go:25, cluster/core/access_control.go:290-292, cluster/core/vid_verification.go:63-65, examples/reference-bridge/persist.go:62-86.
What survives a restart¶
| Survives | Does not survive |
|---|---|
| fabrics, NOCs, keys, ACL | live PASE and CASE sessions (controllers reconnect, with resumption) |
| group keys, group table, Groupcast properties | the event log's events (their numbers continue past the stored ceiling) |
| endpoint numbers, labels, ConfigurationVersions, scene tables, bindings | an open commissioning window (an uncommissioned node opens its own again at Start) |
| CASE resumption records | the diagnostic event ring behind DiagnosticLogs |
| the subscriptions of active CASE sessions, re-established under their old ids | |
| UniqueID, RebootCount, operational time, OTA default providers |
A restart does not re-identify the node: UniqueID is pinned, and fabrics and endpoint numbers are the same.
Source: node/node.go:272-281, node/node.go:500-535, store/schema.sql:250-256, node/security.go:256, node/security.go:364, bridge/bridge.go:85-94, node/config.go:259-274.
The controller database¶
A controller keeps its state in the same kind of file. The reference
controller and the quickstart apply store.Apply (for the settings table)
and sqlstore.Apply:
| Where | Holds |
|---|---|
matter_settings controller.ca |
the CA: root certificate, root key, certificate counter (JSON, matter.js field names) |
matter_settings controller.ca.fabric |
our fabric: fabric id, root node id, IPK, index, label, our key and NOC |
matter_settings controller.dcl.trust |
the DCL trust store (reference controller, with --dcl) |
matter_client_values |
each paired node's attribute values, as TLV |
matter_client_versions |
each stored cluster's DataVersion |
The two controller.ca keys are ca.SettingStore's default; another
Key moves both. Controller-side CASE resumption records are kept in
memory only, so the first command of a run makes a full CASE handshake.
Source: controller/ca/persist.go:90-111, controller/client/sqlstore/sqlstore.go:4-44, controller/client/sqlstore/schema.sql:11-31, examples/reference-controller/cli/stack.go:35-41, examples/reference-controller/README.md (The database), node/client.go:96-101.
Schema changes¶
Each schema ships three ways:
Applyruns the whole DDL. It is idempotent and safe on every boot.store.Applythen runsstore.Upgrade, which rebuilds what an idempotentCREATEcannot change (the v3 rebuild ofmatter_group_key_map).Migrations()returns the same schema as versioned steps (v1,v2, …), each withUpandDown, for a host that runs its own migration tool. A released step never changes. Pinlen(Migrations())and the last version, so the next release's new step fails the pin at the dependency bump instead of at a live write.v3'sDownis lossy.SchemaFingerprint(db)hashes the module's tables and indexes as SQLite records them. Compare it with the value the CHANGELOG records for the release to know your tables match. A host's own tables and indexes do not change it.
| Package | Schema/Apply |
Migrations |
SchemaFingerprint |
|---|---|---|---|
store |
yes | yes | yes |
controller/client/sqlstore |
yes | yes (its own list) | yes |
endpoint/sqlitestore |
yes | no | no |
The schemas are independent and can be applied in either order.
Source: store/schema.go:23-90, store/migrations.go:22-120, controller/client/sqlstore/sqlstore.go:11-16, controller/client/sqlstore/schema.go:31-104, endpoint/sqlitestore/sqlitestore.go:68-72, endpoint/sqlitestore/schema.sql:8-11.
Backup¶
The module ships no backup tool. What a backup has to respect:
- The file holds private keys. The node's operational keys and the controller's CA key are stored unencrypted. The store's documentation leaves file-system protection to the operator. Protect the database and every copy of it like the keys.
- Copy a consistent state. Stop the node first, or use SQLite's own
online mechanisms (
VACUUM INTO, the backup API). Copying the file of a running node can catch a write half-done. - Run one copy at a time. A restored database is the same node: the same fabrics, the same operational identity. Two running copies answer as one node under one identity.
Source: store/doc.go:38-40, store/schema.sql:48-57, examples/reference-controller/README.md (The database).