Skip to main content
Simple Storage is a legacy feature! New apps should use one of the modern alternatives instead:
  • Storage Vault for small encrypted values that need to survive uninstall and sync across devices on the same Apple ID or Google account. Backed by iCloud Key Value Store on iOS and Android Key/Value Backup on Android, with optional biometric gating.
  • IndexedDB (built into the WebView, no Despia SDK needed) for general-purpose structured local storage with multiple object stores, indexes, and transactions. Works in any browser and inside the Despia runtime.
  • PowerSync for a full SQLite database that stays in sync with your backend in real time. Best for apps with non-trivial offline state, multi-device sync, or relational data.
Simple Storage only stores a single string slot per app and has no encryption, no sync, and no structure. The schemes below remain supported for apps already using them, but anything new is better served by one of the alternatives above.
The legacy writevalue:// and readvalue:// schemes write and read a single URL-encoded string slot on the device. The data persists between app sessions and is keyed to your app’s bundle, with no sub-keys and no structured access. Pass JSON-stringified data in, parse JSON-stringified data out.

Installation

npm install despia-native
import despia from 'despia-native';

How it works

There is one storage slot per app. writevalue:// overwrites it, readvalue:// returns it. To store anything richer than a single string, JSON-encode an object before writing and JSON-decode it after reading.
import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

async function save(data) {
    if (!isDespia) return
    const encoded = encodeURIComponent(JSON.stringify(data))
    await despia(`writevalue://${encoded}`)
}

async function load() {
    if (!isDespia) return null
    const result = await despia('readvalue://', ['storedValues'])
    if (!result.storedValues) return null
    return JSON.parse(decodeURIComponent(result.storedValues))
}
The encodeURIComponent and decodeURIComponent calls matter. The scheme parser treats &, =, and # as URL syntax, so any of those characters in your JSON will corrupt the stored value without encoding.

Write data

Stringify, encode, write. The whole operation is one round trip with no return value to handle on the write side.
const userData = {
    refreshToken: 'abc123',
    lastSeen:     Date.now(),
}

const encoded = encodeURIComponent(JSON.stringify(userData))
if (isDespia) {
    await despia(`writevalue://${encoded}`)
}
Writing replaces whatever was previously stored. There is no append, no merge, and no per-key write. If you need to update one field of a larger object, read the full slot, mutate the field in memory, and write the updated object back.
async function updateLastSeen() {
    if (!isDespia) return

    const current = await load()
    const next    = { ...(current || {}), lastSeen: Date.now() }

    const encoded = encodeURIComponent(JSON.stringify(next))
    await despia(`writevalue://${encoded}`)
}

Read data

Wait for the read to resolve, then parse. The result object is keyed on whatever key name you passed in the second argument, so ['storedValues'] returns { storedValues: '...' }.
async function readData() {
    if (!isDespia) return null

    const result = await despia('readvalue://', ['storedValues'])
    if (!result.storedValues) return null

    try {
        return JSON.parse(decodeURIComponent(result.storedValues))
    } catch {
        return null
    }
}
The try/catch around JSON.parse is worth doing reflexively. Stored data can become corrupt if the URL encoding was skipped on write, or if you change the schema without migrating old values, and a parse error should not crash your app.

Do not block UI on the initial read

The first time a user opens your app, there is nothing stored yet. Treat the empty case as the default state, not as a loading state. Render your UI immediately, then upgrade it once data arrives.
import { useEffect, useState } from 'react'

function App() {
    const [user, setUser] = useState(null)

    useEffect(() => {
        readData().then(setUser)
    }, [])

    // render the UI now, with or without user data
    return (
        <div>
            {user ? <p>Welcome back, {user.name}</p> : <p>Welcome</p>}
        </div>
    )
}
Showing a blocking loading spinner before the first read finishes makes new users wait for nothing, since there is no data to retrieve on a fresh install.

Browser fallback

In a regular browser the SDK call is a no-op behind the isDespia guard. Fall back to localStorage for web users so the same save and load functions work everywhere.
async function save(data) {
    const json = JSON.stringify(data)

    if (isDespia) {
        await despia(`writevalue://${encodeURIComponent(json)}`)
        return
    }

    localStorage.setItem('appData', json)
}

async function load() {
    if (isDespia) {
        const result = await despia('readvalue://', ['storedValues'])
        if (!result.storedValues) return null
        try {
            return JSON.parse(decodeURIComponent(result.storedValues))
        } catch {
            return null
        }
    }

    const json = localStorage.getItem('appData')
    return json ? JSON.parse(json) : null
}
Both paths return the same shape, so the rest of your app does not need to know which storage backend handled the call.

Resources

NPM Package

despia-native

Storage Vault

Encrypted, synced, biometric-gated alternative

PowerSync

Full SQLite local database with backend sync