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

# State persistence

> How to persist state across Mini App sessions. Cookies do not work in the sandboxed iframe environment. Use localStorage or IndexedDB instead.

Mini Apps run as sandboxed cross-origin iframes inside the Startale App. This environment imposes browser-level storage restrictions that differ from a normal browser tab. Understanding them upfront prevents silent bugs in production.

## What works and what does not

| Mechanism                              | Works | Notes                                                                                              |
| -------------------------------------- | ----- | -------------------------------------------------------------------------------------------------- |
| `localStorage`                         | Yes   | Recommended. Stable across sessions from `app.startale.com`.                                       |
| `IndexedDB`                            | Yes   | Same origin rules as `localStorage`. Good for structured or large data.                            |
| `sessionStorage`                       | Yes   | Not persisted across page reloads or frame re-opens. Suitable for ephemeral in-session state only. |
| JavaScript cookies (`document.cookie`) | No    | Silently dropped on Safari and iOS due to Intelligent Tracking Prevention. See below.              |
| `Set-Cookie` from your backend         | No    | Same cross-origin restriction: cookies set by a third-party origin are blocked in iframes.         |
| Storage Access API                     | No    | Not available. The iframe sandbox is missing `allow-storage-access-by-user-activation`.            |

## Why cookies fail silently

Mini App URLs are always cross-origin relative to `app.startale.com` (all Mini Apps are hosted on their own domain). The Startale App embeds them with:

```
sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin"
```

The `allow-same-origin` flag preserves the Mini App's own origin (so the Mini App can access its own `localStorage`), but the frame is still third-party to `app.startale.com`.

**Safari / iOS, Intelligent Tracking Prevention:** ITP blocks third-party cookie storage in cross-origin iframes by default. `document.cookie = "..."` runs without throwing an error, but the cookie is silently dropped or scoped to ephemeral storage that does not survive navigation. This is not a bug in the Startale App; it is Safari's enforced policy.

**Chrome, CHIPS (Partitioned Cookies):** Third-party cookies require the `Partitioned` attribute. Without it, behavior is unreliable and being phased toward full removal.

**The key misconception:** `SameSite=None; Secure` controls whether a cookie is *sent* on cross-site requests. It does not override ITP or Partitioned Cookie policies that block the cookie from being *stored* in the first place.

## localStorage: the recommended approach

`localStorage` works reliably in the sandboxed iframe because `allow-same-origin` preserves the Mini App's origin. Storage is scoped to your Mini App's origin and is stable across repeated sessions launched from `app.startale.com`.

```ts theme={null}
// Persist state
localStorage.setItem('game:round', JSON.stringify({ round: 5, score: 1200 }))

// Restore on re-open
const saved = localStorage.getItem('game:round')
const state = saved ? JSON.parse(saved) : { round: 1, score: 0 }
```

**Namespace your keys.** Use a consistent prefix to avoid collisions between multiple Mini Apps on the same origin:

```ts theme={null}
const STORAGE_PREFIX = 'your-app-name:'

const save = (key: string, value: unknown) =>
  localStorage.setItem(`${STORAGE_PREFIX}${key}`, JSON.stringify(value))

const restore = <T>(key: string, fallback: T): T => {
  const raw = localStorage.getItem(`${STORAGE_PREFIX}${key}`)
  return raw !== null ? (JSON.parse(raw) as T) : fallback
}
```

## Saving state on close

The reliable hooks for catching a frame close or navigation are `visibilitychange` and `pagehide`. Register them on mount and write to `localStorage` synchronously inside the handler.

```ts theme={null}
import { sdk } from '@farcaster/miniapp-sdk'

function saveState(state: GameState) {
  localStorage.setItem('your-app-name:state', JSON.stringify(state))
}

// Register on mount
const handleHide = () => {
  if (document.visibilityState === 'hidden') {
    saveState(currentState)
  }
}

const handlePageHide = () => {
  saveState(currentState)
}

document.addEventListener('visibilitychange', handleHide)
window.addEventListener('pagehide', handlePageHide)
```

<Warning>
  Write synchronously inside `pagehide`. The browser may freeze the JavaScript thread immediately after the event fires, so async operations including `IndexedDB` writes are not reliable at this point. Use `localStorage.setItem`; it is synchronous.
</Warning>

Restore state on mount before calling `sdk.actions.ready()`:

```tsx theme={null}
useEffect(() => {
  const saved = localStorage.getItem('your-app-name:state')
  if (saved) {
    restoreState(JSON.parse(saved))
  }
  sdk.actions.ready()
}, [])
```

## Backend authentication: replace cookies with Bearer tokens

If your Mini App authenticates against a backend API, return the session token in the JSON response body and store it in `localStorage`. Do not rely on `Set-Cookie`.

```ts theme={null}
// Auth flow: store token from response body, not from Set-Cookie
const { token } = await fetch('https://api.your-app.com/auth', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address }),
}).then((r) => r.json())

localStorage.setItem('your-app-name:auth_token', token)

// Subsequent authenticated requests
const data = await fetch('https://api.your-app.com/deposit', {
  headers: {
    Authorization: `Bearer ${localStorage.getItem('your-app-name:auth_token')}`,
  },
}).then((r) => r.json())
```

Your backend must accept `Authorization: Bearer <token>` instead of reading from the `Cookie` header.

## IndexedDB: structured or large data

For structured data, larger payloads, or binary assets, use `IndexedDB`. It follows the same origin rules as `localStorage` and works reliably inside the iframe.

```ts theme={null}
import { openDB } from 'idb'

const db = await openDB('your-app-name', 1, {
  upgrade(db) {
    db.createObjectStore('state')
  },
})

// Write
await db.put('state', { round: 5, score: 1200 }, 'game')

// Read
const saved = await db.get('state', 'game')
```

<Warning>
  Do not write to IndexedDB inside a `pagehide` handler. `IndexedDB` operations are asynchronous and will not complete before the browser freezes the thread. Use `localStorage.setItem` for any state that must be saved on close.
</Warning>
