Persistent store (`store_*`)
A reboot-persistent key-value store for PHP, backed by the SoC's NVS flash. Write a value in one boot and read it back in the next — boot counters, provisioning flags, calibration constants, last-known state.
store_* is a small key-value store that survives resets. Write a value from PHP and it comes back on the next boot — a boot counter, a last-known state, a provisioning flag, a calibration value:
$boots = (int) store_get('boots', '0') + 1;
store_set('boots', (string) $boots);
It is backed by the SoC's NVS (Non-Volatile Storage): a wear-levelled, power-loss-safe key-value area in flash. The store is written by the running script and changes at runtime — it is the read-write counterpart to the read-only, build-time .env environment, and the reboot-surviving counterpart to the volatile in-RAM mem_* table.
The store-demo example is a boot counter you can watch climb across resets.
Enabling it
The store needs a slice of flash, so it is off by default. Give it a size in the project config:
[store]
size_kb = 32 # size of the persistent NVS partition; 0 or absent = no persistence
phpflash passes this size into the build as -DPHP_STORE_KB, and the partition generator (cmake/gen-partitions.cmake) adds a dedicated phpstore NVS partition to the generated table. This is independent of the embedded-vs-microSD choice: a project can run its source from flash or from an SD card and still have a persistent store either way.
With no [store] section (or size_kb = 0) no partition is generated, store_available() returns false, and the other functions are inert — they return false/null/[] rather than raising an error, so the same script runs unchanged with or without persistence configured.
How the partition is sized
The requested size_kb is rounded up to NVS's 4 KB page alignment and then floored at 16 KB — NVS needs a few pages for bookkeeping before it holds any user data, so anything smaller is bumped up to that floor. A 32 KB partition holds a few hundred small entries.
size_kb in config |
Effect |
|---|---|
absent, or 0 |
No phpstore partition; store_available() is false; all writes/reads inert. |
1–16 |
Rounded up to the 16 KB minimum (NVS needs the headroom). |
> 16 |
Rounded up to the next 4 KB boundary and used as-is. |
The build log prints the resolved size, e.g. php-esp32: persistent 'phpstore' NVS partition = 32K.
API
The store extension is built into the firmware and exposes seven functions in the global namespace:
| Function | Returns | Notes |
|---|---|---|
store_set(string $key, string $value) |
bool |
Persist a value; auto-committed on return. |
store_get(string $key, ?string $default = null) |
?string |
The value, or $default (or null) if absent. |
store_has(string $key) |
bool |
Whether the key currently exists. |
store_delete(string $key) |
bool |
Remove one key; auto-committed. |
store_clear() |
bool |
Wipe every key in the store. |
store_keys() |
array |
The list of keys currently stored. |
store_available() |
bool |
Is persistence configured and ready? |
store_set
store_set
Writes $value under $key and commits it to flash immediately, so the value is durable the moment the call returns — safe even across an abrupt power cut. Returns false if persistence is not configured, if the key is empty or longer than 15 characters, or if the underlying NVS write fails.
$key
The entry name. At most 15 characters (an NVS limit); a longer or empty key is rejected.
$value
The value to persist. Values are strings; NVS caps a single string near 4 KB.
store_get
store_get
Reads the value stored under $key. Returns the stored string, or $default when the key is absent, when persistence is not configured, or when the key is invalid. With no $default given, a missing key yields null.
$key
The entry name to read.
$default
Returned verbatim when the key is not present. Defaults to null.
store_has
store_has
Reports whether $key currently exists in the store. Returns false for a missing key, an invalid key, or when persistence is not configured.
$key
The entry name to test.
store_delete
store_delete
Removes $key and commits the change to flash. Returns false if the key is invalid, if it does not exist, or if persistence is not configured.
$key
The entry name to remove.
store_clear, store_keys, store_available
store_clear
Erases every key in the store's namespace and commits. Returns false when persistence is not configured.
store_keys
Returns a list of the keys currently stored — exactly the keys the script wrote, since all entries live in a single dedicated NVS namespace. Returns an empty array when the store is empty or unconfigured.
store_available
Returns true when a phpstore partition exists and was opened successfully at boot. Use it to branch on whether persistence is present before relying on the other calls.
Keys, values, and durability
- Keys are at most 15 characters — an NVS constraint (
NVS_KEY_NAME_MAX_SIZEis 16 including the terminator). A longer or empty key is rejected andstore_setreturnsfalse. - Values are strings. Store a number as
(string)and read it back with(int)/(float); store a structure withjson_encode()and read it withjson_decode(). NVS caps a single string near 4 KB. - Writes commit immediately. Both
store_setandstore_deletecallnvs_commit()before returning, so a value is durable the instant the call returns — there is no separate flush step and no data loss window on power failure. - One namespace. Every entry lives in a single NVS namespace inside the
phpstorepartition, sostore_keys()andstore_clear()see precisely what the script wrote and nothing from the system's own NVS.
// a number
store_set('threshold', (string) 42);
$threshold = (int) store_get('threshold', '0');
// a small structure
store_set('cfg', json_encode(['ssid' => 'lab', 'ch' => 6]));
$cfg = json_decode(store_get('cfg', '{}'), true);
Example: a boot counter
The store-demo example bumps a counter on every boot, writes a one-time message on the first boot, and lists the stored keys — the classic demonstration that the value lives in flash rather than RAM:
function setup(): void
{
echo "\n=== store-demo :: persistent boot counter ===\n";
if (!store_available()) {
echo " persistence is OFF -- add [store] size_kb to php-esp32.config.toml\n";
echo "=============================================\n";
return;
}
$boots = (int) store_get('boots', '0') + 1; // values are strings; cast to use them
store_set('boots', (string) $boots);
if ($boots === 1) {
store_set('first_msg', 'hello from boot #1');
}
printf(" boot count : %d (survives resets)\n", $boots);
printf(" first_msg : %s\n", store_get('first_msg', '(none)'));
printf(" keys : %s\n", implode(', ', store_keys()));
echo "=============================================\n";
}
Flash it, reset the board a few times, and the count climbs across every reset:
=== store-demo :: persistent boot counter ===
boot count : 4 (survives resets)
first_msg : hello from boot #1
keys : boots, first_msg
Note the guard on store_available(): the demo runs even without persistence configured, printing a hint instead of failing. Building store_available() checks into any script that uses the store keeps it portable between builds that ship a [store] partition and builds that don't.
What it is — and is not — for
Use the persistent store for configuration and state that must survive a reboot: counters, feature flags, the last reading, a device identity, a provisioning token, a calibration constant. These are written rarely and read often, which is exactly the access pattern flash is good at.
Not a log for high-frequency writes
NVS is wear-levelled, but flash cells still wear out with each erase/write cycle. Do not store_set on every loop tick or every HTTP request — a hot write loop will eventually exhaust the partition. For counters, caches, and rate-limit buckets that change constantly, write to the volatile in-RAM mem_* table instead: it touches no flash and is designed to be written on every request. For bulk or streaming data (readings, files, a database), use the microSD. Reserve store_* for values that genuinely need to persist and change slowly.
How it relates to the other state stores
| You want to keep... | Use | Survives reboot? | Touches flash? |
|---|---|---|---|
| Config/state written rarely, read often | store_* (this page) |
Yes | Yes (wear-levelled) |
| A volatile scalar/array written every request | mem_* |
No | No |
| Read-only config baked in at build time | .env |
Yes (immutable) | Compiled into firmware |
| Bulk / relational / streaming data | microSD (files, SQLite via PDO) | Yes | SD card |
How it works
The store extension is compiled into the firmware and initialised in its module startup. At boot it opens the phpstore NVS partition; on a brand-new or version-mismatched partition it erases and re-formats it once, then opens it. If the partition is not present at all — persistence not configured — initialisation stays quiet and inert, and every function short-circuits on an internal readiness flag (surfaced to PHP as store_available() returning false).
All entries are stored as NVS strings in a single namespace within that partition, which is why store_keys() enumerates exactly the script's own keys and store_clear() wipes only them, leaving the system NVS untouched.