Save-game persistence for Capacitor games, on SQLite everywhere.
Translation pending
Developer toolsTypeScript · JavaScriptGitHub @jbcomOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Save-game persistence for Capacitor games on SQLite everywhere: sql.js and IndexedDB on the web, native SQLite or SQLCipher on iOS and Android.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Save-game persistence for Capacitor games. One API
stores saves in SQLite on every platform: jeep-sqlite
over sql.js and IndexedDB on the web, and native SQLite
(optionally SQLCipher-encrypted) on iOS and Android.Read the full README ↗Save-game persistence for Capacitor games. One API stores saves in SQLite on every platform: jeep-sqlite over sql.js and IndexedDB on the web, and native SQLite (optionally SQLCipher-encrypted) on iOS and Android.
@capacitor-community/sqlite, opened
lazily and isolating corrupt rows instead of failing the whole list.N → N+1 steps that are validated, frozen
and walked to the exact target version, so old saves load safely.@capacitor/preferences for small
settings.Documentation · API · Architecture · Changelog
npm install persistence-save @capacitor/core @capacitor-community/sqlite @capacitor/preferences
The three Capacitor packages are peer dependencies of the root entry. The 0.2
line is verified against @capacitor/core 8.5, @capacitor-community/sqlite
8.1 and @capacitor/preferences 8.0. Node.js 22, 24 and 26 are supported.
The autosave scheduler and the snapshot-migration walker have their own
entries, persistence-save/autosave and persistence-save/migrations, which
load no Capacitor code. The Capacitor peers are optional, so a game without
Capacitor installs persistence-save alone and imports those. A game on
another Capacitor major can import them too, but an optional peer is still
checked when it is present: the install reports the peers unmet (a warning by
default, an error under strict-peer-dependencies). The root entry needs the
8.x peers.
On the web, copy both WebAssembly assets into the directory you pass as
wasmAssetsPath (for example with your bundler's static-copy step):
node_modules/persistence-save/dist/assets/sql-wasm.wasm
node_modules/persistence-save/dist/assets/sql-wasm-browser.wasm
They are also exported as persistence-save/assets/sql-wasm.wasm and
persistence-save/assets/sql-wasm-browser.wasm.
import { createPersistence } from 'persistence-save';
interface State {
name: string;
gold: number;
}
interface Snapshot extends State {
version: number;
}
const saves = createPersistence<State, Snapshot>({
dbName: 'com.example.game_v1',
snapshotVersion: 2,
serialize: (state) => ({ version: 2, ...state }),
deserialize: ({ name, gold }) => ({ name, gold }),
migrations: {
1: (old) => ({ ...old, version: 2, gold: old.gold ?? 0 }),
},
});
await saves.save('Slot 1', { name: 'Player One', gold: 7 });
const [latest] = await saves.list(); // newest first; corrupt rows are isolated, not thrown
const record = latest ? await saves.load(latest.id) : null;
console.log(record?.snapshot.gold); // 7
Every source version, target version and migration output is a safe integer.
Each migration receives a deeply frozen clone and is a pure N → N+1 step.
Mutation attempts, null or non-object output, skips, overshoots, fractions, and
a walk that does not end exactly at the target are rejected as typed version
errors. Reading an older save migrates it in memory without rewriting the stored
row; only an explicit save() writes the current format.
serialize() is validated before SQLite is opened: it must return a non-array
object whose version exactly equals snapshotVersion. Snapshot size caps are
measured in UTF-8 bytes on both read and write.
Games with their own SQLite tables can pass an idempotent
initializeSchema(connection) callback and use withConnection() and flush()
for their own statements. This keeps one connection manager and one web-store
flush path. encryptionKeyPreference keeps an existing native SQLCipher key
name when you adopt the package in a game that already encrypts its database.
Preferences store strings, so serialize structured values explicitly, and always use a stable reverse-domain namespace:
import { createPreferencesKv } from 'persistence-save';
const settings = createPreferencesKv('com.example.game');
await settings.set('audio.muted', 'false');
import { createAutoSaveScheduler } from 'persistence-save';
const autosave = createAutoSaveScheduler({
provider: () => (inMenu ? null : currentState()), // null skips the write
save: (state) => saves.save('AutoSave', state),
debounceMs: 400,
});
autosave.schedule(); // after a mutation; bursts coalesce into one write
autosave.scheduleThrottled('player.health', 2_000); // per-frame callers
addEventListener('pagehide', () => void autosave.flush()); // always saves
await autosave.flushPending(); // wait for a queued save; writes nothing if none is
autosave.pending; // true while a debounced or throttled save is queued
The scheduler takes any save, so it works over your own storage too, from
the Capacitor-free entry:
import { createAutoSaveScheduler } from 'persistence-save/autosave';
import { migrateSnapshot } from 'persistence-save/migrations';
See the API reference for every option.
mise install # or: npm install --global corepack && corepack enable
pnpm install
pnpm verify
pnpm verify runs Biome, markdownlint, strict TypeScript, the tests with
coverage, the ESM, CommonJS and type builds, publint, Are The Types Wrong, and
a packed-consumer proof. That proof packs the package, installs the tarball with
the exact Capacitor peers into a clean project with an empty home directory and
only the public registry, then runs the ESM and CommonJS entry points,
typechecks a separately written consumer, and compiles both WebAssembly assets.
pnpm test:web-cold-reload separately packs the built package, starts a clean
Vite consumer and uses a headed, muted Chromium session to prove that a flushed
save has IndexedDB bytes and reopens in a freshly navigated JavaScript realm.
Set PERSISTENCE_SAVE_CONSUMER_SOURCE=persistence-save@<version> to run the same
proof against a published version.
See CONTRIBUTING.md. Releases are automated with Release Please and published to npm with provenance.
MIT. The bundled sql.js WebAssembly binaries are MIT-licensed by the sql.js authors; see THIRD_PARTY_NOTICES.md.