Mod Storage

New in v1.0.0

Persist settings and data across game sessions (Electron only)

<Callout type="info"> Storage is only available in the desktop (Electron) version. In browser, storage operations are no-ops that return safely without error. </Callout>

The Storage API lets your mod save settings and data that persist across game sessions. Data is stored per-mod in the user's app data directory.

<Callout type="warning"> Common gotcha: storage.get() returns a Promise, not the value directly. If you forget await, you'll silently get [object Promise] instead of your data.

javascript
// Wrong — val is a Promise object, not your data
const val = api.storage.get('key');

// Correct
const val = await api.storage.get('key');

</Callout>

Save and Load Settings#

Grab a scoped storage object at the top of your script — it stays tied to your mod across await:

javascript
const storage = window.SubwayBuilderAPI.storage.scoped();

// Save a setting
await storage.set('myModEnabled', true);
await storage.set('favoriteColor', '#ff0000');
await storage.set('userPrefs', {
    volume: 0.8,
    notifications: true
});

// Load a setting (with optional default value)
const enabled = await storage.get('myModEnabled', false);
const color = await storage.get('favoriteColor', '#000000');
const prefs = await storage.get('userPrefs', {});

console.log('Mod enabled:', enabled);

Delete and List Keys#

javascript
// Delete a stored value
await storage.delete('myModEnabled');

// Get all stored keys for this mod
const keys = await storage.keys();
console.log('Stored keys:', keys); // ['favoriteColor', 'userPrefs']

Async Code: Use storage.scoped()#

Storage calls are tied to your mod's identity, which the game only knows while your mod's code is executing synchronously. After the first await (or inside a setTimeout/setInterval callback), that context is gone — plain storage.* calls log a "called outside mod context" warning, writes are dropped, and reads return the default value.

Call storage.scoped() synchronously at the top of your script or callback to capture your mod's identity into a storage object that keeps working across await:

javascript
const api = window.SubwayBuilderAPI;
const storage = api.storage.scoped(); // capture context while it's available

(async () => {
    await storage.set('greeting', 'hello');
    const value = await storage.get('greeting'); // works — 'hello'
    const keys = await storage.keys();
})();

Alternatively, every storage method accepts your mod ID (from your manifest.json) as a trailing argument:

javascript
await api.storage.set('greeting', 'hello', 'my-awesome-mod');
const value = await api.storage.get('greeting', undefined, 'my-awesome-mod');

API Reference#

storage.get(key, defaultValue?, modId?)#

Retrieves a stored value.

ParameterTypeDescription
keystringThe key to retrieve
defaultValueanyValue to return if key doesn't exist
modIdstringOptional: your mod ID, for use after an await

Returns: Promise<any> - The stored value or default

storage.set(key, value, modId?)#

Stores a value.

ParameterTypeDescription
keystringThe key to store under
valueanyThe value to store (must be JSON-serializable)
modIdstringOptional: your mod ID, for use after an await

Returns: Promise<void>

storage.delete(key, modId?)#

Deletes a stored value.

ParameterTypeDescription
keystringThe key to delete
modIdstringOptional: your mod ID, for use after an await

Returns: Promise<void>

storage.keys(modId?)#

Lists all stored keys for this mod.

ParameterTypeDescription
modIdstringOptional: your mod ID, for use after an await

Returns: Promise<string[]> - Array of key names

storage.scoped()#

Captures the current mod's identity and returns a storage object (get/set/delete/keys, plus a modId property) that keeps working in async code. Must be called synchronously — at the top of your mod script or at the start of a hook callback, before any await.

Returns: Scoped storage object

Data Types#

Storage supports any JSON-serializable data:

javascript
// Primitives
await storage.set('count', 42);
await storage.set('name', 'My Mod');
await storage.set('enabled', true);

// Objects
await storage.set('config', {
    theme: 'dark',
    notifications: true,
    favorites: ['NYC', 'CHI', 'LA']
});

// Arrays
await storage.set('history', [
    { date: '2024-01-01', action: 'installed' },
    { date: '2024-01-02', action: 'configured' }
]);

<Callout type="warning">Functions, DOM elements, and circular references cannot be stored.</Callout>

Mod Isolation#

Each mod's storage is isolated based on the mod ID from your manifest.json:

json
{
    "id": "my-awesome-mod",
    "name": "My Awesome Mod"
}

Keys are automatically namespaced, so storage.set('config', ...) in your mod won't conflict with another mod using the same key name.

Per-Playthrough Data#

Storage is global to your mod — it is not automatically scoped to a save. If your mod tracks data that belongs to one playthrough (ridership history, custom objectives, per-city progress), key it by the game session ID: a UUID that is generated when a new game starts, written into every save, and stable across saves, loads, and renames.

javascript
const api = window.SubwayBuilderAPI;
const storage = api.storage.scoped();

api.hooks.onGameLoaded(async () => {
    const runId = api.gameState.getGameSessionId();
    const history = await storage.get(`run:${runId}:history`, []);
    // ...append this session's data, then:
    await storage.set(`run:${runId}:history`, history);
});

<Callout type="warning"> Don't key playthrough data by getSaveName() — the name is null before the first save, changes when the player renames or "saves as", and collides when two playthroughs share a name. getGameSessionId() has none of these problems. See Game State. </Callout>

Complete Example#

javascript
const api = window.SubwayBuilderAPI;
const storage = api.storage.scoped(); // capture mod context for async use

// Mod settings panel with persistent preferences
const DEFAULT_SETTINGS = {
    showNotifications: true,
    autoSave: true,
    theme: 'auto'
};

// Load saved settings on init
api.hooks.onGameInit(async () => {
    const settings = await storage.get('settings', DEFAULT_SETTINGS);
    applySettings(settings);
});

// Settings UI
api.ui.registerComponent('settings-menu', {
    id: 'my-mod-settings',
    component: api.utils.components.createElement(
        'div',
        { className: 'space-y-4' },
        api.utils.components.createElement(api.utils.components.Switch, {
            id: 'notifications',
            defaultChecked: true,
            onCheckedChange: async (checked) => {
                const settings = await storage.get('settings', DEFAULT_SETTINGS);
                settings.showNotifications = checked;
                await storage.set('settings', settings);
            }
        }),
        api.utils.components.createElement(
            api.utils.components.Label,
            { htmlFor: 'notifications' },
            'Show Notifications'
        )
    )
});

function applySettings(settings) {
    // Apply your mod's settings
    console.log('Applied settings:', settings);
}

Browser Compatibility#

When running in the browser (not Electron), storage operations are safe no-ops:

javascript
// In browser, these work but don't persist:
await api.storage.set('key', 'value'); // Does nothing
await api.storage.get('key', 'default'); // Returns 'default'
await api.storage.keys(); // Returns []

This lets you write code that works in both environments without conditionals.