Plain rules. We want this to feel like the old portals — fun, weird, and safe to click.
.html file. Inline CSS, JS, and
assets (data: URIs are fine).
Your game can persist up to 64 KB of JSON per save slot, scoped
to the signed-in player — nobody else can read or write it. Games
have no network access at all (see the sandboxing note above), so
saves go through the parent page via postMessage.
Paste this into your HTML and call Save.get() /
Save.put(data):
<script>
let _saveReqId = 0;
const _saveWaiters = new Map();
window.addEventListener("message", (e) => {
const m = e.data;
if (!m || m.channel !== "domainless-save" || m.type !== "result") return;
const w = _saveWaiters.get(m.reqId);
if (!w) return;
_saveWaiters.delete(m.reqId);
m.ok ? w.resolve(m.data) : w.reject(new Error(m.error));
});
function _saveCall(type, extra) {
return new Promise((resolve, reject) => {
const reqId = ++_saveReqId;
_saveWaiters.set(reqId, { resolve, reject });
window.parent.postMessage(
Object.assign({ channel: "domainless-save", type, reqId }, extra),
"*",
);
});
}
const Save = {
get: (slot = "default") => _saveCall("get", { slot }),
put: (data, slot = "default", version = 1) =>
_saveCall("put", { slot, data, version }),
del: (slot = "default") => _saveCall("delete", { slot }),
list: () => _saveCall("list", {}),
};
// await Save.put({ level: 3, coins: 120 });
// const saved = await Save.get(); // { slot, version, data, updated_at } or null
</script>
Not signed in? Save.get() / Save.put()
reject with an error your game can catch (e.g. show "sign in to save
progress"). Slot names: letters, numbers, - and
_, up to 40 characters — use more than one slot for
multiple save files. version is yours to use however
you like (e.g. bump it when you change your save format).
Questions? Email us.