Identity
Identity is one sign-in for every game on the network: a player creates one ARRR account, and any app can ask them to sign in to it. The account's keys are made and unlocked on the player's own device; the network never holds them, so no node — and not even central, on its own — can read what a player saves. This guide is for app owners. If you are looking for the save data itself, see Player data.
What the player sees
identity.login() opens a small popup on cloud.arrr.fun: sign in with a username and password, or with Discord, Google or X. The popup does the actual key work — deriving and unlocking the account's keys — and closes itself, handing your page back a session:
const session = await arrrNetwork.identity.login({ appId: 'your-app-id' });
// opens the ARRR sign-in popup on cloud.arrr.fun// session: { userId, username, token, expiresAt, ... }If the browser blocks the popup, login() falls back to a full-page redirect back to your app. Either way, your code only ever sees the resolved session — it never sees a password, and it never receives the player's master key unless your app is a trusted first party (the portal and the console; ordinary games are not).
Your page must be served over https (or from localhost while developing): the keys are handled with WebCrypto, which browsers provide only to a secure origin. The session token your page receives is scoped to your app: it joins your rooms as the player and reads their holders, and it cannot change the account, sign in to another game, or open the console.
Which sites may sign players in to your app is up to you. List them under Allowed origins in the console and the sign-in page refuses any other site ("This site is not allowed to sign in to <your game>"). Leave the list empty and the page instead asks the player to confirm the site by name before handing your page a session.
Staying signed in
arrrNetwork.identity.current({ appId }) // session or null, restored from localStorage
arrrNetwork.identity.logout({ appId })current() reads the session your page already has, restored from localStorage, and returns null once it has expired — call login() again to renew it. logout() clears it locally; it does not sign the player out of other apps or out of the identity page itself (sessions are stateless JWTs, so there is no server-side revocation to call). The player signs out of the identity page — forgetting their keys on that browser — at cloud.arrr.fun/id/#/account.
What a node can and cannot see
Every game's saves are held on ordinary nodes — anyone may run one — so the design assumes a node is never trusted with a player's data:
| A node sees | A node never sees |
|---|---|
An opaque userId (a hash of the account's public key) | The player's username or email |
Opaque record ids (recId, a per-app HMAC) | Which game a record belongs to |
| Ciphertext sizes, padded to 1 KiB buckets | The size of the actual document |
| Write times and the client's IP | A byte of the save itself |
A node stores ciphertext and signatures; it verifies a write's signature but cannot decrypt a read. That holds even if a node operator is actively hostile — they can refuse service, but not read or forge.
The trust ceiling: password vs. OAuth and reset
Signing in with a password is the strongest tier: the key that unwraps the account's master key is derived from the password alone, on the player's device, and nobody else ever has it — not central, not a node, not the ARRR team.
Signing in with OAuth, or resetting a password by email, is one notch weaker: those paths recover the master key through a login key that is Shamir-split across the player's holder nodes, released only against a token central mints. That means central and k holder nodes (2 of 3 by default) would have to collude to reconstruct it. A single hostile node, or central alone, still cannot. Central prefers holders with different owners, but owner diversity is best-effort: on a single-box deployment all holders share one operator, and the guarantee is then only as strong as that operator.
The index record
Every time a player signs in to an app, the identity page writes one entry — { name, lastSeenAt } — into a record only it can write: the index record. It lists every app the player has signed in to, encrypted the same way as any other save, and it is how a page like the portal's "my games" can say "here is everywhere you have played" without any individual game being able to read, or forge an entry in, that list.
Check it worked
arrrNetwork.identity.onChange(session => console.log('signed in as', session?.username));Sign in once, then reload the page and call identity.current({ appId }) — you should get the same session back with no popup, because it was restored from localStorage, and the returned session should carry a username. If you open the popup window's own devtools during sign-in, its network tab shows the POST /api/id/session that the popup makes to cloud.arrr.fun answering 200 with token and user; a 401 there means the signature or the nonce did not check out. A refused origin never reaches that request at all — it shows up as a message inside the popup, not a network call or a status code.
Next steps
- Player data — save and load a document per player, per app
- Publish on arrr.fun — put a game in front of players who already have an account
- API Keys — the app id an identity session is scoped to
