storage
Per-mod key-value persistence backed by a JSON file per mod id in the Electron main process.
api.storage. 5 members. Each get, set, delete and keys call resolves a mod id from the active mod context, falling back to the explicit modId argument, then invokes a mod-storage-* IPC channel. The file is <userData>/mod-data/<id>.json, where characters outside [a-zA-Z0-9._-] in the id are percent-encoded per UTF-8 byte. A name longer than 255 bytes is shortened to at most 200 bytes: a prefix of the encoded id, ~, and the first 16 hex digits of the SHA-256 of the id. When that file cannot be read, a legacy file with those characters replaced by _ is read instead, which another id may have written. The legacy file is never written; the first write copies its keys into the new file. Every call reads the whole file. Writes are serialized per mod id and rewrite the whole file. Outside Electron window.electron.invoke is absent, so reads return the default and writes do nothing.
| Member | Returns | Summary |
|---|---|---|
get(key, defaultValue?, modId?) | Promise of T or undefined | Read one key from the mod's storage file. |
set(key, value, modId?) | Promise<void> | Write one key to the mod's storage file. |
delete(key, modId?) | Promise<void> | Remove one key from the mod's storage file. |
keys(modId?) | Promise<string[]> | Every key in the mod's storage file. |
scoped() | object | Capture the active mod id at call time into an object whose methods pass it as the modId fallback. |
get#
api.storage.get(key: string, defaultValue?: T, modId?: string): Promise<T | undefined>Read one key from the mod's storage file.
The mod id used is the active mod context when one is set, otherwise modId. An explicit modId is ignored while a mod id is active: during the synchronous part of a mod script and of each hook callback registered while one was active. Resolves to the stored value, or to defaultValue when the key is absent, the stored value is undefined, the IPC call fails, or no mod id is available.
| Param | Type | Default | Description |
|---|---|---|---|
key | string | required | storage key; property name in the mod's JSON file. |
defaultValue | T | none | value resolved when nothing is stored; default undefined. |
modId | string | none | fallback mod id used only when no mod context is active. |
Returns: Promise<T | undefined>. Promise of the stored value (parsed from JSON, a fresh object per call) or defaultValue.
Errors:
- no mod id: logs
[Modding API] storage.get called outside of mod context, resolves todefaultValue. - IPC returns an error: logs
[Modding API] Error getting storage value:, resolves todefaultValue. - call throws: logs
[Modding API] Error getting storage value:, resolves todefaultValue. - outside Electron: resolves to
defaultValuewithout logging.
api.storage.get('settings', { notifications: true }).then((settings) => {
console.log('Notifications on:', settings.notifications);
});set#
api.storage.set(key: string, value: T, modId?: string): Promise<void>Write one key to the mod's storage file.
The mod id used is the active mod context when one is set, otherwise modId. An explicit modId is ignored while a mod id is active: during the synchronous part of a mod script and of each hook callback registered while one was active. The main process reads the file, sets the key and rewrites the whole file as 2-space JSON. Writes for one mod id run one after another.
| Param | Type | Default | Description |
|---|---|---|---|
key | string | required | storage key; property name in the mod's JSON file. |
value | T | required | value to store; must survive JSON.stringify. |
modId | string | none | fallback mod id used only when no mod context is active. |
Returns: Promise<void>.
Errors:
- no mod id: logs
[Modding API] storage.set called outside of mod context, does nothing. - IPC returns an error: logs
[Modding API] Error setting storage value:, nothing stored. - call throws: logs
[Modding API] Error setting storage value:, nothing stored. - outside Electron: does nothing without logging.
Side effects: writes <userData>/mod-data/<id>.json; the main process logs [Mods] Stored <key> for mod <id>.
// any JSON-serializable value; call it synchronously or use storage.scoped() after an await
api.storage.set('lastPlayed', { day: api.gameState.getCurrentDay(), at: Date.now() });delete#
api.storage.delete(key: string, modId?: string): Promise<void>Remove one key from the mod's storage file.
The mod id used is the active mod context when one is set, otherwise modId. An explicit modId is ignored while a mod id is active: during the synchronous part of a mod script and of each hook callback registered while one was active. Deleting an absent key still writes the file.
| Param | Type | Default | Description |
|---|---|---|---|
key | string | required | storage key to remove. |
modId | string | none | fallback mod id used only when no mod context is active. |
Returns: Promise<void>.
Errors:
- no mod id: logs
[Modding API] storage.delete called outside of mod context, does nothing. - IPC returns an error: logs
[Modding API] Error deleting storage value:, nothing removed. - call throws: logs
[Modding API] Error deleting storage value:, nothing removed. - outside Electron: does nothing without logging.
Side effects: rewrites <userData>/mod-data/<id>.json; the main process logs [Mods] Deleted <key> for mod <id>.
api.storage.delete('lastPlayed');keys#
api.storage.keys(modId?: string): Promise<string[]>Every key in the mod's storage file.
The mod id used is the active mod context when one is set, otherwise modId. An explicit modId is ignored while a mod id is active: during the synchronous part of a mod script and of each hook callback registered while one was active.
Parameter modId (string): fallback mod id used only when no mod context is active.
Returns: Promise<string[]>. Promise of a fresh array of key names; an empty array when the file is missing, the IPC call fails, or no mod id is available.
Errors:
- no mod id: logs
[Modding API] storage.keys called outside of mod context, resolves to[]. - IPC returns an error: logs
[Modding API] Error getting storage keys:, resolves to[]. - call throws: logs
[Modding API] Error getting storage keys:, resolves to[]. - outside Electron: resolves to
[]without logging.
api.storage.keys().then((keys) => console.log(`stored keys: ${keys.join(', ')}`));scoped#
api.storage.scoped(): {…}Capture the active mod id at call time into an object whose methods pass it as the modId fallback.
Each method calls the global API's storage method with the captured id, so the active mod context still wins when another mod's script or callback is running at that moment. With no active mod context the captured id is null and the methods behave like the unscoped ones.
Returns: object. Object with get, set, delete, keys bound to the captured id and a modId field holding it.
| Field | Type | Description |
|---|---|---|
get | <T>(key: string, defaultValue?: T) => Promise<T | undefined> | storage.get with the captured mod id as the fallback; defaultValue is resolved when nothing is stored. |
set | <T>(key: string, value: T) => Promise<void> | storage.set with the captured mod id as the fallback. |
delete | (key: string) => Promise<void> | storage.delete with the captured mod id as the fallback. |
keys | () => Promise<string[]> | storage.keys with the captured mod id as the fallback. |
modId | string or null | Mod id captured when scoped() was called; null when it was called outside mod context. |
Errors: no mod context: logs [Modding API] storage.scoped() called outside of mod context, returns an object with modId null.
const store = api.storage.scoped(); // at the top of the script, before any await
setTimeout(async () => {
await store.set("lastRun", Date.now()); // still writes to this mod's file
}, 1000);Data Types#
A value crosses IPC to the main process and is written with JSON.stringify, so store JSON data: strings, numbers, booleans, null, arrays and plain objects. What get returns is parsed from the file, a fresh copy on every call.
| Value | Stored as |
|---|---|
| string, number, boolean, null | itself |
| array, plain object | JSON; nested values follow the same rules |
Date | ISO string |
Map, Set, class instance | {} (own enumerable fields only for instances) |
undefined (as the value or in an object field) | dropped; get resolves defaultValue for a dropped key |
| function, DOM element | the IPC call fails; logs [Modding API] Error setting storage value:, nothing stored |
NaN, Infinity | null |
const store = api.storage.scoped();
store.set('config', { theme: 'dark', favorites: ['NYC', 'CHI'] }).then(() => store.get('config', {})).then((config) => console.log(config.favorites));Mod Isolation#
Each mod has one file, <userData>/mod-data/<id>.json, keyed by the id in its manifest.json. Two mods can use the same key name without conflict. While a mod's script or one of its hook callbacks runs, every call uses that mod's file and an explicit modId is ignored, so a mod cannot read another mod's file from inside its own code.
| Case | File name |
|---|---|
id of a-z, A-Z, 0-9, ., _, - | the id itself |
| other characters | percent-encoded per UTF-8 byte: a b becomes a%20b.json |
| encoded name over 255 bytes | an encoded prefix, ~, and the first 16 hex digits of the SHA-256 of the id, at most 200 bytes |
| no file under the new name | the legacy name (those characters replaced by _) is read; the first write creates the new file |
File names ignore case on macOS and Windows, so ids that differ only in case share a file there.
Per-Playthrough Data#
Storage belongs to the mod and is shared by every save. Key playthrough data by gameState.getGameSessionId(): a UUID set when a new game starts, written into every save and restored on load. getSaveName() is a poor key: it is null before the first save and changes on rename.
const store = api.storage.scoped(); // capture the mod id before any await
api.hooks.onGameLoaded(async () => {
const runId = api.gameState.getGameSessionId();
const history = await store.get(`run:${runId}:history`, []);
history.push({ day: api.gameState.getCurrentDay() });
await store.set(`run:${runId}:history`, history);
});Browser Compatibility#
Outside Electron there is no main process: get resolves defaultValue, keys resolves [], and set and delete do nothing, all without logging. The same code runs in both environments.
Complete Example#
Persistent settings with a toggle in the settings screen. storage.scoped() is captured at the top of the script, because storage calls made after an await (or from a React event handler) have no mod context of their own.
const api = window.SubwayBuilderAPI;
const store = api.storage.scoped(); // before any await
const DEFAULTS = { showNotifications: true, bonusEvery: 100 };
async function loadSettings() {
return { ...DEFAULTS, ...(await store.get('settings', DEFAULTS)) };
}
async function saveSetting(key, value) {
const settings = await loadSettings();
settings[key] = value;
await store.set('settings', settings);
}
api.hooks.onGameLoaded(async () => {
const settings = await loadSettings();
if (settings.showNotifications) api.ui.showNotification('Settings loaded', 'info');
});
api.ui.addToggle('settings-menu', {
id: 'my-mod-notifications',
label: 'Show my mod notifications',
defaultValue: true,
onChange: (enabled) => saveSetting('showNotifications', enabled),
});