> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elata.bio/llms.txt
> Use this file to discover all available pages before exploring further.

# App State

> Per-user, per-app key-value storage with @elata-biosciences/app-state.

<Warning>
  `@elata-biosciences/app-state` is not published to npm yet. Until it is, you can call Elata directly with the `elata:state:*` messages shown below; Elata already supports them.
</Warning>

## Overview

`@elata-biosciences/app-state` stores small JSON values per user and per app:
save games, settings, progress. You don't need a backend, and your app never
sees the user's identity. Values follow the user across devices.

```bash theme={null}
# Not on npm yet. This command works after the first release.
npm install @elata-biosciences/app-state
```

```ts theme={null}
import { getState, setState, deleteState } from '@elata-biosciences/app-state';

// null if nothing was ever stored
const save = await getState('save_slot_1');

await setState('save_slot_1', { level: 7, hp: 100 });

// idempotent: no error if the key was never set
await deleteState('save_slot_1');
```

***

## Limits

| Limit | Value |
| - | - |
| Key | 1 to 256 characters from `A-Z a-z 0-9 . _ : / -` |
| Value | Any JSON-serializable value, up to 64 KB once encoded |
| Scope | One namespace per (user, app). Apps can't read each other's data. |

***

## Errors

All three functions reject with `AppStateError`, which has a `code`:

| Code | Where it comes from | Meaning |
| - | - | - |
| `no_parent` | Local | Not running inside Elata's frame |
| `timeout` | Local | No reply in time |
| `invalid_input` | Local or Elata | Bad key or value |
| `not_authenticated` | Elata | The user isn't signed in |
| `value_too_large` | Elata | Value is over 64 KB |
| `fetch_failed` | Elata | Elata couldn't reach storage |

<Tip>
  Players who aren't signed in get `not_authenticated`. Keep a local fallback,
  such as `localStorage`, and sync to `app-state` once they sign in.
</Tip>

***

## Without the package

Elata answers these messages directly. Send them to `window.parent` with a unique `requestId` and wait for the matching `…:result` reply:

```js theme={null}
function stateCall(type, payload) {
  return new Promise((resolve, reject) => {
    const requestId = crypto.randomUUID();
    const onMessage = (e) => {
      if (e.data?.type !== `${type}:result` || e.data.requestId !== requestId) return;
      window.removeEventListener('message', onMessage);
      e.data.error ? reject(new Error(e.data.error)) : resolve(e.data);
    };
    window.addEventListener('message', onMessage);
    window.parent.postMessage({ type, requestId, ...payload }, '*');
  });
}

const { value } = await stateCall('elata:state:get', { key: 'save_slot_1' });
await stateCall('elata:state:set', { key: 'save_slot_1', value: { level: 7 } });
await stateCall('elata:state:delete', { key: 'save_slot_1' });
```

Add a timeout in production: outside Elata nothing replies.

***

## app-state or app-metrics?

| Use | Package |
| - | - |
| The latest value of something: settings, a save slot, unlocked levels | `app-state` |
| A history of events or scores you want to query and chart | [`app-metrics`](/apps/platform/metrics-and-scores) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.