Skip to content

Storage ​

Two global key/value stores keep data between launches. Both are scoped to your source (and to each instance when allowsMultipleInstances is on), and both store any JSON value.

StoreKept inSyncedIn Mana backupsUse for
ObjectStoreMana's app preferences on the deviceNoOnly when the user includes "Sensitive source data"Settings, caches, IDs, timestamps
SecureStoreiOS KeychainYes, through iCloud KeychainNoTokens, passwords, API keys
Where values are stored
Each store is scoped to the source instance · values are saved as JSON text

Values stay when the source is updated or uninstalled, so a reinstalled source finds its old settings and tokens. Clear what you no longer need with remove.

API ​

ts
// Write any JSON value.
await ObjectStore.set("lastSync", Date.now());
await ObjectStore.set("session", { token: "abc", expires: 1767225600 });
await SecureStore.set("password", "hunter2");

// Read. Returns null when missing.
const session = (await ObjectStore.get("session")) as { token: string; expires: number } | null;

// Remove.
await ObjectStore.remove("session");

// Typed reads: throw if the stored value has another type.
const token = await SecureStore.string("token");             // string | null
const nsfw = await ObjectStore.boolean("nsfw");              // boolean | null
const page = await ObjectStore.number("page");               // number | null
const languages = await ObjectStore.stringArray("languages"); // string[] | null

Every call is a round trip to Mana: the value is turned into JSON text in JavaScript, stored as text, and parsed again when read.

Writing and reading a value
Values cross to Mana as JSON text and are parsed again in JavaScript

The typed reads check the parsed value. They return null for falsy values too (false, 0, ""):

Typed reads
string(), boolean(), number() and stringArray()

For settings that can be false or 0, use get with a default:

ts
const nsfw = ((await ObjectStore.get("nsfw")) as boolean | null) ?? true;

Patterns ​

Typed settings ​

ts
const DEFAULTS = { nsfw: false, quality: "high", languages: ["en"] };
type Settings = typeof DEFAULTS;

async function setting<K extends keyof Settings>(key: K): Promise<Settings[K]> {
  return ((await ObjectStore.get(key)) as Settings[K] | null) ?? DEFAULTS[key];
}

const quality = await setting("quality");

Cache with expiry ​

Cache with expiry
Serve from ObjectStore while fresh · otherwise load and save
ts
async function cached<T>(key: string, ttlMs: number, load: () => Promise<T>): Promise<T> {
  const entry = (await ObjectStore.get(key)) as { at: number; value: T } | null;
  if (entry && Date.now() - entry.at < ttlMs) return entry.value;

  const value = await load();
  await ObjectStore.set(key, { at: Date.now(), value });
  return value;
}

const genres = await cached("genres", 24 * 60 * 60 * 1000, () => this.fetchGenres());

Token storage ​

ts
async function saveTokens(t: { access: string; refresh: string; expiresIn: number }) {
  await SecureStore.set("tokens", { ...t, expiresAt: Date.now() + t.expiresIn * 1000 });
}

async function accessToken(): Promise<string | null> {
  const t = (await SecureStore.get("tokens")) as { access: string; expiresAt: number } | null;
  return t && t.expiresAt > Date.now() ? t.access : null;
}

Keep the stored shape small: each get reads and parses the whole value.