← Back to Documentation Index

Nova64 Multiplayer + Identity Design (`nova64.net` + `nova64.auth`)

Sponsor

Status: design / phased plan — nothing implemented yet. This document is the plan we execute against.

Two new cart-facing namespaces:

They are designed together because auth gates net: a player authenticates once via nova64.auth, and the resulting token authorizes room joins.

Related: VIDEO_GUIDE.md (the same "one cart API, per-backend host implementation" pattern), GODOT_HOST_CONTRACT.md, api-improvements.md.


1. Goals & non-goals

Goals

Non-goals (initially)


2. Architecture overview

   Cart (init/update/draw)
        │  nova64.net / nova64.auth   (identical API on every backend)
        ▼
   ┌─────────────────────────────────────────────────────────┐
   │  Nova64 runtime shim (JS)                                 │
   │   - colyseus.js client (schema decode, room protocol)     │
   │   - auth provider registry                                │
   │   - pluggable transport seam ───────────────┐             │
   └──────────────────────────────────────────────┼───────────┘
            web │                          Godot   │
                ▼                                  ▼
        browser WebSocket             bridge: net.* over Godot WebSocketPeer
        window.ethereum (wallet)      OS.shell_open + loopback (OAuth)
                │                                  │
                └──────────────┬───────────────────┘
                               ▼
              ┌──────────────────────────────────┐
              │  nova64-server (Node)             │
              │   - Colyseus rooms (@schema)      │
              │   - onAuth: verify session JWT    │
              │  nova64-auth (Node)               │
              │   - OAuth code exchange → JWT     │
              │   - wallet nonce/verify (SIWE)→JWT│
              └──────────────────────────────────┘

Key decision — one JS client, two transports. colyseus.js and @colyseus/schema are pure JS and run in both the browser and Godot's QuickJS. The only backend-specific piece is the WebSocket transport:

This keeps the room protocol and schema decoding in one codebase on web (verified — the web lobby works end to end).

Phase 2 Godot — revised (2026-06-22). The "run colyseus.js in QuickJS" idea proved costly: the published colyseus.js bundles (node and the colyseus-cocos-creator engine build) drag in ws/Buffer/process internals (isUtf8, etc.) that don't run in a bare QuickJS sandbox (verified via a sandbox test). Making it work needs a custom browser-clean rollup of colyseus.js's browser entry + Buffer/TextEncoder/process polyfills — a real bundling effort.

The cleaner path (recommended) is the official Colyseus Godot SDK (https://docs.colyseus.io/getting-started/godot) — a native GDExtension addon (GDScript, beta) that already speaks the protocol + schema over WebSockets. Integration shape for Nova64: the Godot host drives the native client and the bridge exposes the same nova64.net surface to the JS cart — net.join, per-frame net.poll returning state diffs (player add/change/remove) + inbound messages, and net.send. The cart code stays identical to web; only the Godot host's net implementation differs (native client instead of a JS WebSocket). Trade-off: a C++/GDScript ↔ QuickJS marshaling layer for state callbacks, vs. the colyseus.js-bundling effort.

Resolved — this is exactly what shipped. See Phase 2 — DONE below; the integration was built and verified headlessly (Godot 4.5, cross-play with a Node client). The marshaling layer is _forward_net (C++) → NovaNet.gd.


3. nova64.net — cart API

Modeled closely on colyseus.js so it's familiar and thin:

// 1) connect (token comes from nova64.auth; optional for guest/dev)
await nova64.net.connect({ url: 'wss://play.nova64.dev', token: nova64.auth.token() });

// 2) join a room (joinOrCreate | create | join | joinById)
const room = await nova64.net.joinOrCreate('arena', { name: 'IO', skin: 3 });

// 3) authoritative state (server-owned). Schema add/remove/change callbacks:
room.state.players.onAdd((p, id) => spawnAvatar(id, p));
room.state.players.onRemove((p, id) => despawn(id));
room.state.players.onChange((p, id) => moveAvatar(id, p.x, p.y));

// 4) messages (client → server intents, server → client events)
room.send('move', { dx, dy });
room.onMessage('hit', e => flash(e.target));

// 5) lifecycle
room.sessionId;                       // this client's id in the room
room.onLeave(code => toLobby());
room.onError((code, msg) => toast(msg));
nova64.net.leave();                   // leave current room
nova64.net.isSupported();             // false on hosts without net

Per-frame pump (native): carts call nova64.net._tick(dt) in update() (no-op on web, drives the Godot transport poll). The reference helpers (nova64.level etc.) and a multiplayer-demo cart will model this.

Helpers we ship on top (reduce per-cart boilerplate):


4. The server — nova64-server (Colyseus)

A Node project (new top-level server/ or repo) running Colyseus.

Room model (phased):

Auth hook: onAuth(client, options) verifies the options.token (session JWT from nova64-auth) — signature, expiry, audience. Rejects on failure; attaches { userId, displayName, provider } to the client. Guest mode (no token) is allowed in dev / for public rooms, gated by config.

Hosting: self-host (Docker) or Colyseus Cloud — a Phase 0 decision (§9).


5. nova64.auth — extensible identity

5.1 Cart API

// sign in with a provider (returns a session)
const s = await nova64.auth.signIn('google');                  // social preset
const s = await nova64.auth.signIn('oauth', {                  // generic OIDC
  issuer: 'https://id.example.com', clientId: '...', scopes: ['openid','profile'],
});
const s = await nova64.auth.signIn('wallet');                  // EVM SIWE

nova64.auth.signOut();
const me = nova64.auth.identity();   // unified profile (below) or null
const jwt = nova64.auth.token();     // session JWT for nova64.net.connect
nova64.auth.onChange(session => updateHud(session));
await nova64.auth.restore();         // silent resume from stored token

// EXTENSIBILITY — register a custom provider
nova64.auth.registerProvider('steam', mySteamProvider);

5.2 Unified identity model

type Identity = {
  id: string;            // stable, provider-namespaced: "google:1234", "wallet:0xabc…"
  provider: string;      // "google" | "discord" | "oauth" | "wallet" | custom
  displayName: string;
  avatar?: string;       // URL or data URI
  address?: string;      // wallet address when provider is crypto
  claims: object;        // verified JWT claims / profile
  token: string;         // session JWT (short-lived) for the server
};

A stable, provider-namespaced id lets the same human link multiple methods later (account linking is a future claim on the server). The server is the source of truth for userId; the client identity mirrors it.

5.3 Provider interface (the extensibility seam)

interface AuthProvider {
  name: string;
  signIn(opts?): Promise<Session>;   // run the flow, return identity + token
  restore?(): Promise<Session|null>; // silent re-auth from stored refresh/token
  signOut?(): Promise<void>;
}

Built-in providers:

5.4 JWT / session

nova64-auth mints a short-lived session JWT (e.g. 15 min) + a refresh token. The session JWT is what nova64.net presents to Colyseus onAuth. Algorithm RS256 (server holds the private key; Colyseus verifies with the public key). Claims: sub (userId), provider, name, iat/exp/aud/iss.


6. Per-backend matrix

Web Godot RetroArch
net transport browser WebSocket bridge net.* over WebSocketPeer (poll each frame) none in-core → §7
net protocol colyseus.js (shared) colyseus.js in QuickJS (shared) —
OAuth flow popup/redirect OS.shell_open + loopback TCPServer (or custom URI scheme) to catch the redirect Device Code grant (RFC 8628): show code+URL, poll
wallet window.ethereum (EIP-1193) SIWE WalletConnect (QR / deep link) out of scope (no secure signer)
token storage localStorage (or cookie) user:// encrypted store core save storage (if pursued)

The cart code is identical; only these host mechanisms differ, hidden behind the shim + bridge.


7. RetroArch (later)

The libretro core has no general socket API, so standard client–server Colyseus is not reachable from inside the core. Options to research as a separate phase (not blocking web/Godot):

  1. libretro netpacket interface (RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE). The frontend moves packets between peers for netplay. This is P2P / lockstep-flavored, not client-server — it could carry a custom rollback protocol but does not map onto Colyseus rooms. Best fit for deterministic head-to-head, not a live authoritative MMO-style room.
  2. Frontend-assisted socket bridge — a non-standard environment callback / custom RetroArch build that proxies a WebSocket to the core. Powerful but forks the frontend; portability cost is high.
  3. Async / turn-based via HTTP — if a future core gains an HTTP fetch hook (frontend-mediated), turn-based or lobby features work even without realtime sockets.

Recommendation: treat RetroArch multiplayer as a research spike after web + Godot ship, most likely landing as netpacket-based rollback for 1v1/local-style carts, with realtime authoritative rooms staying web/Godot only. Document the decision; don't block the main line.


8. Security & ops considerations


9. Decisions (Phase 0)

Locked (2026-06-21):

Still open (revisit before public deploy): production hosting, refresh-token rotation specifics, account-linking across providers.

Original option analysis

  1. Server hosting: self-host (Docker on a VPS) vs Colyseus Cloud. Recommendation: self-host a single small instance for dev; revisit for prod.
  2. Auth issuer: roll our own nova64-auth (full control, more work) vs a managed broker (Auth0 / Clerk / Supabase Auth / Logto) that already does social + JWT, with us adding wallet. Recommendation: managed broker for social/OAuth to move fast; our own thin service only for the wallet/SIWE flow + session minting. Logto/Supabase are self-hostable + support custom flows.
  3. First social provider to wire end-to-end (Discord and Google are easiest for games). Recommendation: Discord.
  4. Wallet scope: EVM-only at first (SIWE) vs EVM+Solana. Recommendation: EVM first, Solana behind the same provider seam later.
  5. Repo layout: server in this monorepo (/server, /auth) vs a separate repo. Recommendation: monorepo for now.
  6. Room model for v1: confirm the generic StateRoom (presence + position + relay) is enough for the first demo.

10. Phased roadmap

Each phase ends with a runnable demo + verification.

Phase 0 — Foundations & decisions

Phase 1 — nova64.net on web + minimal auth

Phase 2 — nova64.net on Godot ✅ DONE (2026-06-22)

Net shipped; Godot OAuth deferred to Phase 3. The "colyseus.js in QuickJS" plan was abandoned (no WebSocket/XHR/Buffer in Godot's QuickJS). Final shape — the official native Colyseus Godot SDK drives the socket; the cart's nova64.net calls are bridged to it:

cart JS → nova64.net (shim, nova64-compat.js)
        → engine.call("net.*")  →  Nova64Host::_forward_net (C++)
        → NovaNet.call_net (GDScript)  →  Colyseus.Client / Room (native SDK)

Stack alignment (required): the Godot SDK speaks colyseus 0.17, so the server moved 0.15→0.17 (ESM-only) and web moved colyseus.js 0.15→0.16.22 (its latest; no 0.17 client exists). 0.17 returns a flat seat reservation that the 0.16 client can't parse, so api-net.js re-nests it (patchSeatReservation).

Recipe for local Godot multiplayer: server cd server && pnpm start; deps install on ext4 + symlink (pnpm EACCES on /mnt/c — see [server/.npmrc]); run Godot with -- ws://<wsl-ip>:2567 multiplayer-lobby (native sockets need the real WSL IP, not localhost). Colyseus plugin enabled in project.godot.

Still TODO (Phase 2 tail): Godot OAuth (OS.shell_open + loopback TCPServer → token; user:// storage) + a Godot nova64.auth shim — deferred to land with Phase 3 providers.

Phase 3 — Wallet + provider extensibility

Phase 4 — Hardening & authoritative rooms

Phase 5 — RetroArch research spike


11. Proposed repo layout

server/                 # Colyseus (nova64-server)
  src/rooms/StateRoom.ts
  src/auth/verify.ts     # session-JWT verification (onAuth)
auth/                    # nova64-auth (or config for a managed broker)
  src/oauth/…            # code exchange + JWT minting
  src/wallet/siwe.ts     # nonce + verify
runtime/api-net.js       # nova64.net facade (colyseus.js + transport seam)
runtime/api-auth.js      # nova64.auth registry + providers
nova64-godot/gdextension/src/bridge.cpp   # net.* (WebSocketPeer), auth helpers
examples/multiplayer-lobby/               # the cross-backend demo cart
docs/MULTIPLAYER_AND_AUTH_DESIGN.md        # this doc

To start: answer the Phase 0 decisions in §9 (especially hosting + auth issuer + first social provider), and I'll build Phase 1 (web nova64.net + the generic StateRoom + one-provider nova64.auth + the lobby demo).