Skip to main content
Building the client is the ownership check. ArcaneClient::init runs it once at launch, for the title named in ARCANE_GAME_ID; you do not add a second ownership step, and you pass nothing. Init asks the Arcane desktop app first, on loopback (127.0.0.1:39284), and opens Arcane via the arcane-powered:// deep link if the app is not running. A launch mints a fresh ticket — that is what the launch is for. The local cache answers exactly one case: the desktop app is running and tells the SDK it cannot reach the cloud.

Game id

The game id Arcane Powered hands your process in ARCANE_GAME_ID is the id of your title in the Arcane portal. It keys the local flag and ticket files, names the title in every desktop route, and must match the ticket’s gid claim. It is read and validated before any I/O: unset or empty is missing_game_id; ASCII letters, digits, _, -, ., at most 256 bytes, and anything else is invalid_game_id. During local development you set the variable yourself with the value from the portal. It is not a secret. The id identifies a title; it proves nothing on its own. What proves ownership is the signed ticket below — an ES256 JWT bound to an account and a device — which nobody can mint from the id.

Default init policy

ArcaneClient::init():
  1. Reads the game id from ARCANE_GAME_ID and validates it
  2. Probes the desktop health endpoint, opening arcane-powered://sdk/ownership?game_id=… when Arcane is not running
  3. Calls the desktop’s ownership refresh, which mints a ticket for this account and this device
  4. Runs the offline check against what the desktop just wrote
  5. If the desktop reported DRM is off for this title and no ticket appeared, resolves to DrmDisabled rather than failing
The only step that falls back to the cache is 3, and only when the desktop answers offline — it is running, it simply cannot reach the cloud. Then the flow becomes: read flags/{game_id}.json, and if DRM is on, run the offline check against the ticket the last launch left on disk. It is verified in full, exactly as a fresh one would be. Anything else the desktop says stops the launch. not_owned and not_authenticated are answers, not gaps, and a cached ticket must not paper over them; arcane_unavailable means the desktop app never spoke at all, which is not a state a game may start in.
This is deliberately not cache-first. A cached ticket that still parses would otherwise outlive what it attests: an account key rotated since it was minted leaves the ticket bound to a fingerprint the machine can no longer present, and device_mismatch does not ask for a refresh. The player would be locked out of a title they own, with no way back that does not involve deleting a file by hand.
Everything learned along the way — user_id, ticket expiry — is kept on the client, alongside the game_id init read from the environment. See Client.

Offline check (what init runs when DRM is on)

  1. Resolves the ticket for the signed-in account (see below)
  2. Rejects clock rollback vs last_seen_wall_time
  3. If drm_enabled on the ticket file is false → DrmDisabled
  4. Ensures the ticket string is non-empty
  5. Verifies the JWT (ES256, issuer arcane-drm, audience arcane-game-sdk)
  6. Requires the dev claim to name a fingerprint this machine can present
The ticket file’s own device_hash field is not checked. It is unsigned, so it proves nothing the dev claim does not.

Which account’s ticket

ARCANE_USER_ID — set by the Arcane desktop app on the game process — names the account it launched the game for. session.json, written on sign-in and sign-out, is the offline source of truth when the variable is absent. The no-fallback rule matters on shared machines. A ticket belonging to a different account is correctly signed and correctly bound to the device, so nothing else would reject it — only the account check does. Naming an account is not a way past the check: the variable selects a file, and the ticket in that file is verified in full.

Desktop refresh errors

When init must refresh online, the desktop (and cloud) can return distinct failures: An unrecognised desktop error code becomes ticket_invalid with the original string preserved in the error’s context.desktop_error, so a newer launcher never produces a silent failure. See Errors for the full table.

Data layout

{app_data} is the OS application data directory (the SDK resolves this internally).

Ticket claims

Device fingerprint

machine_id is a UUID written once under the DRM root (mode 0600 on Unix). device_hash is the first 16 bytes of SHA-256(machine_id), hex-encoded, and is readable from the client as device_hash(). Tickets are bound to that hash so they cannot be copied to another machine.

JWKS

Verification keys come from jwks.json. The JWT kid selects the key when present. If JWKS is missing or the kid is unknown, the SDK returns ticket_invalid naming the resolved path — refresh via the desktop app.

What ownership does not prove

The SDK verifies that this machine, for this account, holds a valid signed entitlement for this title. It does not — and cannot — prove that the process making the call is genuinely your game. Any secret compiled into a game binary is extractable, and the init call itself can be patched out. That is also why the game id is not treated as a secret: hiding it would buy nothing. Treat ownership as a platform integration, not as anti-tamper; enforcement that must hold against a determined attacker belongs on the server.