Fluffer by MotorbunnyDevelopers

Web Launcher SDK

Storage

Keep settings, documents and files on the device with game.state and game.storage: private per app, with limits and error codes.

Your app can keep its own data on the player's device: settings, saves, libraries, recordings. @fluffer/game-kit gives you two tools:

ToolBest forSize
game.state(initial, { persist })A handful of settings and small values (high score, volume, last level)Small
game.storageAnything that grows: lists of documents, saved games, photos, audio, videoUp to 25 MB of documents and 500 MB of files

Both are private to your app and need no capability or consent, because the data never leaves the device.

#Your data is private to your app

Fluffer serves every app from its own address (https://<your-app>.fluffer.app/). Browsers and web views keep storage separate per address, so no other app can open your app's data, and you can't open theirs.

Storage is currently device-only: it isn't synced between devices or backed up to the player's account. If the player reinstalls Fluffer or switches phones, plan for the data to be gone.

#Persistent state with game.state

game.state() creates a reactive state object. Keys listed in persist are written to localStorage on every change and loaded again on the next launch.

const state = game.state(
  { score: 0, lives: 3, highScore: 0, volume: 0.8 },
  { persist: ['highScore', 'volume'] },
);

state.current.score += 1;                       // mutate directly
state.subscribe('highScore', (next, prev) => showBanner(next));
state.replace({ score: 0, lives: 3 });          // update several keys
state.reset();                                  // back to the initial values; persisted keys reload
OptionDefaultDescription
persistnoneKeys to save. Everything else is kept in memory only.
storageKey'fluffer:game:'Prefix for the localStorage keys

Persisted values must be JSON. Use game.state for a few small values; localStorage is small and shared by everything in your app.

#Documents and files with game.storage

game.storage stores JSON documents in named collections, and blobs (binary files) under keys. It's backed by IndexedDB in the web view.

Pass your app's slug to createGame() so your database has a stable name:

const game = await createGame({ slug: 'my-game', capabilities: ['device.vibrate'] });

#Documents

interface SaveSlot { level: number; coins: number; name: string }

const saves = game.storage.collection<SaveSlot>('saves');

const doc = await saves.put('slot-1', { level: 3, coins: 120, name: 'Morning run' });
// doc: { collection: 'saves', id: 'slot-1', data: {...}, version: 1, updatedAt: '2026-...' }

const slot = await saves.get('slot-1');        // StoredDoc<SaveSlot> or null
const all = await saves.list();                 // newest first
await saves.delete('slot-1');                   // resolves whether or not it existed

const unsubscribe = saves.subscribe((change) => {
  // { collection, id, version, deleted, origin: 'local' }
  refreshSaveList();
});
MethodReturnsNotes
collection<T>(name)StorageCollection<T>
get(id)StoredDoc<T> or null
put(id, data, { ifVersion }?)StoredDoc<T>Creates or replaces. data must be JSON, at most 256 KB.
delete(id, { ifVersion }?)void
list()StoredDoc<T>[]Most recently written first
subscribe(listener)unsubscribe functionCalled after each write to this collection
interface StoredDoc<T> {
  collection: string;
  id: string;
  data: T;
  version: number;     // goes up by one on every write
  updatedAt: string;   // ISO timestamp of the last write
}

#Safe updates with ifVersion

Pass the version you last read to make sure nobody else changed the document in between. If it changed, the write fails with CONFLICT and nothing is overwritten. Use ifVersion: 0 to mean "only create, never overwrite".

import { FlufferError } from '@fluffer/game-kit';

const current = await saves.get('slot-1');
try {
  await saves.put('slot-1', { ...current!.data, coins: current!.data.coins + 10 }, { ifVersion: current!.version });
} catch (err) {
  if (err instanceof FlufferError && (err.code as string) === 'CONFLICT') {
    // reload and try again
  }
}

await saves.put('slot-2', { level: 1, coins: 0, name: 'New' }, { ifVersion: 0 });   // fails if slot-2 exists

#Blobs

const photo = await new Promise<Blob>((r) => canvas.toBlob((b) => r(b!), 'image/jpeg', 0.9));

const info = await game.storage.blobs.put('photos/run-42.jpg', photo);
// info: { key: 'photos/run-42.jpg', mime: 'image/jpeg', bytes: 183422, updatedAt: '...' }

const back = await game.storage.blobs.get('photos/run-42.jpg');   // Blob or null
const photos = await game.storage.blobs.list('photos/');           // metadata only, no bytes
await game.storage.blobs.delete('photos/run-42.jpg');
MethodReturnsNotes
blobs.put(key, blob, { mime }?)StoredBlobAt most 50 MB each. mime defaults to the blob's type.
blobs.get(key)Blob or null
blobs.delete(key)void
blobs.list(prefix?)StoredBlob[]Sorted by key; doesn't load the bytes

#Usage and persistence

const { docsBytes, blobBytes, docsQuota, blobQuota, synced } = await game.storage.usage();
// synced is false: data lives on this device only

if (!game.storage.isPersistent()) {
  // No IndexedDB here: the API still works, but nothing survives a reload.
}

#Names and limits

ItemRule
Collection nameStarts with a lowercase letter; lowercase letters, digits, _ and -; up to 32 characters
Document idLetters, digits, _ and -; 1 to 64 characters
Blob keyLetters, digits, _, ., / and -; 1 to 128 characters (use / for folders)
One document256 KB of JSON
One blob50 MB
All documents25 MB per app
All blobs500 MB per app

These are also exported as STORAGE_LIMITS.

#Errors

Storage calls reject with a FlufferError whose code is one of:

CodeWhen
INVALID_ARGUMENTBad collection name, document id or blob key, or data that isn't JSON
TOO_LARGEA document over 256 KB or a blob over 50 MB
QUOTA_EXCEEDEDYour app's quota is full, or the device is out of space
CONFLICTifVersion didn't match. The current document is attached to the error as cause.

#Testing

createStorage() and createMemoryBackend() let you build an in-memory store for unit tests:

import { createStorage, createMemoryBackend } from '@fluffer/game-kit';

const storage = createStorage({ backend: createMemoryBackend() });
await storage.collection('saves').put('a', { level: 1 });

#Saving to the photo library

game.storage keeps files inside your app. To save a photo or video to the player's camera roll, where they can see and share it, use game.media with the media.photos capability. See Bridge protocol.