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", {}),
};
// Your game cannot show a sign-in form itself (no network, opaque
// origin), so ask the page around you to do it instead.
const Auth = {
state: () => _saveCall("authstate", {}),
signIn: (reason) => _saveCall("signin", { reason }),
};
// await Save.put({ level: 3, coins: 120 });
// const saved = await Save.get(); // { slot, version, data, updated_at } or null
</script>
Asking the player to sign in. A rejected save now
carries needsAuth: true when the only problem was that
nobody is signed in, so you can tell that apart from a real failure.
Call await Auth.signIn("Sign in to keep your progress.")
and the sign-in sheet opens over your game; it resolves to
{ signedIn }, which is false if the player
backed out. Auth.state() tells you where you stand
without prompting anyone. Nothing navigates away, so a player never
loses their place mid-run.
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.