Local Storage Persistence with Data Blocks

Architectural overview of saving and loading application state on the user's local device using DataBlockOperator and indexedDB.
Published: 7/10/2026

To persist configuration details, offline drafts, or user options across browser refreshes, the Aircada SDK provides local storage integration.

Rather than accessing browser APIs like localStorage or indexedDB directly, Aircada encapsulates this logic in a built-in DataBlockOperator with dataStoreType: "LOCAL_STORAGE". Under the hood, this operator relies on indexedDB to handle robust, asynchronous database storage.

Key Principles of Local Data Persistence

1. Store Integration: Local storage should operate in tandem with the Aircada Store. On system setup, load stored data and patch the global store. On store changes or trigger events, write the current store state back to the local storage operator.

2. Handling Flat Storage Mappings: The local storage engine works best with flat key-value records. Nested states must be flattened upon saving, and unflattened when loaded back into the store.

3. Local Media Storage: Media items configured with PropertySystemType.MEDIA_ITEM are automatically handled by Aircada's underlying media vault, storing them offline in indexedDB blob storage and restoring them automatically.

Local Data Manager System Example#

A complete TypeScript example demonstrating how to orchestrate the DataBlockOperator to persist and retrieve state from local storage.
typescript
// src/systems/LocalDataManager.ts

import {
    System, Context, Setup, Input, Dispose,
    AircadaContext,
    DataBlockOperator,
    PropertySystemType
} from "@aircada/spec";

@System({
    name: "Local Data Manager",
    type: "local_data_manager"
})
export class LocalDataManager {

    @Context()
    air!: AircadaContext;

    private operator!: DataBlockOperator;

    private isApplyingLoad = false;

    @Setup()
    async setup() {
        console.log("[LocalDataManager] Setup initializing...");

        const defaultConfig = this.air.store.state.testData || {
            id: "random_id", // In this example, we'll be storing with a static constant definedKey string (below)
            mediaItemId: "",
            floatValue: 0.5,
            stringValue: "Default String",
            details: {
                description: "Default Description"
            }
        };

        try {
            // Create separate configuration and default data objects first, then merge them.
            const operatorConfig = {
                definedKey: "local_saved_state", // this will override our ID value - allowing passing id, but storing in a constant, defined location
                dataStoreType: "LOCAL_STORAGE",
                tableName: "test_data_table",
                saveType: "KEY",
                loadType: "KEY"
            };

            const defaultData = {
                id: defaultConfig.id,
                // To benefit from Aircada's underlying media management system we must specify the system type and value
                // Media saved with this system is automatically stored in indexedDB blob storage and loaded automatically
                mediaItemId: { systemType: PropertySystemType.MEDIA_ITEM, value: defaultConfig.mediaItemId },
                floatValue: defaultConfig.floatValue,
                stringValue: defaultConfig.stringValue,
                // Flattening nested string property for local storage
                details_description: defaultConfig.details?.description || "Default Description"
            };

            // Instantiate built-in DATA_BLOCK operator by merging properties
            this.operator = await this.air.operators.create("DATA_BLOCK", {
                // Operator's inbuilt properties
                ...operatorConfig,
                // Our custom data object properties
                ...defaultData
            });

            if (!this.operator) {
                console.error("[LocalDataManager] Failed to create DataBlockOperator.");
                return;
            }

            console.log("[LocalDataManager] DataBlockOperator created successfully.");

            // Listen for loaded data from indexedDB
            this.operator.loadedData.addListener((data: Record<string, any>) => {
                console.log("[LocalDataManager] Loaded data block:", data);
                if (data) {
                    this.isApplyingLoad = true;
                    this.air.store.patch({
                        testData: {
                            id: data.id ?? "",
                            // The actual Media data will have already been imported by this point and associated with this mediaItemId
                            mediaItemId: data.mediaItemId ?? "",
                            floatValue: Number(data.floatValue ?? 0.5),
                            stringValue: data.stringValue ?? "",
                            // Unflattening nested string property from local storage
                            details: {
                                description: data.details_description ?? ""
                            }
                        }
                    });
                    this.isApplyingLoad = false;
                }
            });

            this.operator.onSuccess.addListener(() => {
                console.log("[LocalDataManager] DataBlockOperator operation succeeded.");
            });

            this.operator.onFailure.addListener(() => {
                console.error("[LocalDataManager] DataBlockOperator operation failed.");
            });

            // Trigger a load operation to see if data already exists in indexedDB
            console.log("[LocalDataManager] Loading data from storage...");
            this.operator.load();
        } catch (err) {
            console.error("[LocalDataManager] Error during DataBlockOperator setup:", err);
        }
    }

    @Input({ name: "On Trigger Save Event", onEvent: "TRIGGER_SAVE" })
    onTriggerSave() {
        if (!this.operator || this.isApplyingLoad) return;

        const current = this.air.store.state.testData;
        console.log("[LocalDataManager] Saving state to indexedDB:", current);

        this.operator.dataInput({
            id: current.id,
            mediaItemId: current.mediaItemId,
            floatValue: current.floatValue,
            stringValue: current.stringValue,
            // Flattening nested string property for local saving
            details_description: current.details.description
        });

        // Trigger the save() operation on the operator
        this.operator.save();
    }
}