Skip to main content

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.

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

MechanismWorksNotes
localStorageYesRecommended. Stable across sessions from app.startale.com.
IndexedDBYesSame origin rules as localStorage. Good for structured or large data.
sessionStorageYesNot persisted across page reloads or frame re-opens. Suitable for ephemeral in-session state only.
JavaScript cookies (document.cookie)NoSilently dropped on Safari and iOS due to Intelligent Tracking Prevention. See below.
Set-Cookie from your backendNoSame cross-origin restriction: cookies set by a third-party origin are blocked in iframes.
Storage Access APINoNot 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 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.
// 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:
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.
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)
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.
Restore state on mount before calling sdk.actions.ready():
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.
// 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.
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')
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.