Skip to content

gamesgamesgamesgames.games Intent to Launch

CP
Jul 17, 2026 · 11 min read

s we look into building more interconnected games on the Atmosphere we want to make it easy to find and play these games in a social manner.

The Pentaract provides some of the primitives for describing games, cartridge.dev and stream.place leverages these, but we could go further. Imagine seeing "3 friends are playing this game" on Cartridge, then with one click you can join them in a multi-player arena. Imagine watching someone streaming a game on stream place and then with one click joining them during a tense battle.

I'm proposing a Launch "record" that provides information for systems like Stream Place or Cartridge to launch a game. Additionally we'll extend this system to allow a system like Cartridge to become a "Game Host" that will simplify Authorization.

Games as DIDs

In order to build a game launch we'll need each game to have a unique identifier that can be tied to metadata about the game. DIDs are a perfect candidate for these, but we should also allow records in a PDS - effectively each game can pre represented by either an ATURI which can specify either a bare did, handle, or full path.

Why should we support both DIDs and Records? The persisted data we'll need will be incredibly small, a signing key and a launch host. Requiring this data to be in a record means potentially requiring a whole PDS for a single record. On the flip side a DID web or DID plc is not always reasonable to create for a game, and managing rotation keys can becomes an on-going concern. Because a DID document is just JSON we can simply store it as-is in a repo under a games.gamesgamesgamesgames.identity record.

What's in the DID Doc

The document whether stored in a record or directly on a DID contains the following information.

  1. The host for a launch compatible service
  2. A public signing key that the service will use

To Launch a game we do the following.

  1. A User - Anonymous or identified by a DID, selects a game to Launch / Join
  2. The games.gamegamegamegame.game record includes an ATURI for the launch config.
  3. The ATURI is resolved to a DID document or Record containing a DID document
  4. The launch service host is invoked with an XRPC launch intent call containing the game aturi, as well as basic player information (did) and a join code
  5. The Launch service responds with a singed response (leverage badges.blue) including a launch URL
  6. The launch URL is opened on the users system

The Launch URL can be any valid URL - a system launching a game should trust these urls if they're part of a correctly signed grant. This allows for a few things.

  • A Launch service could be a gateway to a non-atmospheric game, handling did translation and returning a url for a different host
  • A Launch service can handle things such as geo routing or load balancing directing to specific servers

The Lexicon for the xrpc endpoint implemented by the launch service is this:

{
    "lexicon": 1,
    "id": "games.gamesgamesgamesgames.launch.intent",
    "defs": {
      "main": {
        "type": "procedure",
        "description": "Ask a launch service for a signed launch grant. Called against the host from the service document a game's launch config AT-URI resolved to. The response
  carries the grant payload and a detached signature, verifiable against the service document's signingKey; on success the client opens the grant's
  URL.",
        "input": {
          "encoding": "application/json",
          "schema": {
            "type": "object",
            "required": ["game"],
            "properties": {
              "game": {
                "type": "string",
                "format": "at-uri",
                "description": "AT-URI of the game record being launched or joined."
              },
              "player": {
                "type": "string",
                "format": "did",
                "description": "DID of the player launching or joining. Omit if the user is not logged in."
              },
              "joinCode": {
                "type": "string",
                "maxLength": 64,
                "description": "Join code for an existing session. Omit to launch a new one."
              }
            }
          }
        },
        "output": {
          "encoding": "application/json",
          "schema": {
            "type": "object",
            "required": ["grant", "attestation"],
            "properties": {
              "grant": {
                "type": "ref",
                "ref": "#grant"
              },
              "attestation": {
                "type": "ref",
                "ref": "#attestation"
              }
            }
          }
        },
        "errors": [
          {
            "name": "GameNotFound",
            "description": "The game AT-URI did not resolve to a game this service can launch."
          },
          {
            "name": "InvalidJoinCode",
            "description": "The join code did not match a joinable session."
          },
          {
            "name": "PlayerNotAllowed",
            "description": "The service declined to issue a grant to this player DID."
          },
          {
            "name": "LaunchUnavailable",
            "description": "The service cannot currently produce a launch URL (capacity, region, or upstream outage)."
          }
        ]
      },
      "grant": {
         "type": "object",
        "description": "The launch grant the service attests to. A client that verifies the attestation against the launch service's published signing key should trust and open
  `url`, whatever host it points at (gateways to non-atmosphere games, geo routing, load balancing).",
        "required": ["url", "game", "issuedAt"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The launch URL to open on the player's system. Any valid URL; need not be on the launch service's own host."
          },
          "game": {
            "type": "string",
            "format": "at-uri",
            "description": "AT-URI of the game record this grant launches."
          },
          "player": {
            "type": "string",
            "format": "did",
            "description": "DID of the player the grant was issued to."
          },
          "joinCode": {
            "type": "string",
            "maxLength": 64,
            "description": "The join code this grant was issued against, if one was supplied."
          },
          "issuedAt": {
            "type": "string",
            "format": "datetime",
            "description": "ISO 8601 timestamp at which the grant was issued."
          },
          "expiresAt": {
            "type": "string",
            "format": "datetime",
            "description": "ISO 8601 timestamp after which the grant (and its URL) should no longer be trusted."
          }
      },
      "attestation": {
        "type": "object",
        "description": "A badge.blue CID-First attestation over `grant`. The signing payload is a CID: the grant with a `$sig` metadata object inserted (carrying `key` and the
  issuing service's DID as `repository`) is serialized to DAG-CBOR, SHA-256 hashed, and wrapped as a CIDv1 (dag-cbor codec); `signature` is an ECDSA signature (P-256, P-384, or
  K-256; normalized to low-S form) over those CID bytes. Verifiers recompute the CID from the returned grant, check it equals `cid`, resolve `key`, and verify the signature —
  rejecting if `key` is not the signing key published in the launch service's document.",
        "required": ["key", "cid", "signature"],
        "properties": {
          "key": {
            "type": "string",
            "description": "did:key reference to the public key that produced the signature. Must match the signing key in the launch service's document."
          },
          "cid": {
            "type": "string",
            "format": "cid",
            "description": "The attestation CID that was signed, recomputable from `grant`."
          },
          "signature": {
            "type": "bytes",
            "description": "Low-S-normalized ECDSA signature over the CID bytes. (Lexicon `bytes` serializes to JSON as the badge.blue `{\"$bytes\": ...}` wrapper.)"
          }
        }
      }
    }
  }

Simplifying Authorization

There are lots of ways that games can become "atmospheric" but for many games there are some basic capabilities that they'll leverage when writing to a player's PDS. The most obvious are save files.

The current authorization scheme used by PDS would require each game to obtain OAuth consent before writing a save. This has several drawbacks

  1. Granularity of scope - the current granularity available via OAuth requires a user to grant "write anything to this NSID" permissions, or worse "delete anything from this NSID"
  2. Broken Flow - A user clicks "join my friends" and is redirected to an OAuth Consent Screen
  3. Diffusion of Trust - If each game requires a seperate OAuth consent screen players get numb to reading the screens and thinking about if they actually trust the game.

It would be better if for most games a user could simply click "Launch" and be taken to the game. If doing so on a trusted website like Cartridge the user should not worry about untrusted games having destructive access to their PDS.

Game Host

We can extend our Launch record slightly to include optional support for a Game Host. This host provides a series of xrpc endpoints that games can use to write data to a User's PDS or call services on behalf of the user. The Game host mediates these calls and provides additional security by reducing the scope of these calls to a reasonably allowed set. For instance a game may write save records only for this game, the game host determines a unique save record rkey and limits writes from a game to that rkey.

We start with a simple addition of an optional host service to our launch intent request.

{
   "defs": {
     "hostService": {
        "type": "object",
        "required": ["service"],
        "properties": {
          "service": {
            "type": "string",
            "format": "uri",
            "description": "Base URL of the xrpc game host"
          },
          "nonce": {
             "type": "string",
             "maxLength": 128,
             "description": "Optional single-user value to prevent replay attacks"
          }
        }
     }
   }
}

The host service is included both in the launch intent and then echoed back in the grant. In addition the grant includes a jkt

"jkt": {
  "type": "string",
  "description": "RFC 7638 thumbprint of the game-server key this grant may be redeemed with."
}

Once a game launches it proceeds to call a capabilities endpoint using a DPoP handshake. The capabilities endpoint is the only xprc method that the host is required to implement.

{
    "lexicon": 1,
    "id": "games.gamesgamesgamesgames.launch.getCapabilities",
    "defs": {
      "main": {
        "type": "query",
        "description": "List the XRPC services this host provides to the authenticated session. Served by the client-provided service named in a launch grant (`grant.service`);
  callable only over a DPoP-bound session established by presenting that grant, and each request must carry a valid DPoP proof. The result is scoped to the session: it reflects
  what this grant's session may actually call, not everything the host implements.",
        "output": {
          "encoding": "application/json",
          "schema": {
            "type": "object",
            "required": ["capabilities"],
            "properties": {
              "capabilities": {
                "type": "array",
                "description": "One entry per service available to this session. Open union: unrecognized members should be ignored, and the baseline #service member merely
  names an NSID; future members may attach service-specific metadata.",
                "items": {
                  "type": "union",
                  "closed": false,
                  "refs": ["#service"]
                }
              }
            }
          }
        },
        "errors": [
          {
            "name": "AuthRequired",
            "description": "The request did not carry a valid DPoP proof for an established session."
          },
          {
            "name": "SessionExpired",
            "description": "The session this DPoP key was bound to has ended or its grant has expired; the game must obtain a new grant."
          }
        ]
      },
      "service": {
        "type": "object",
        "description": "Baseline capability entry: an XRPC service this host provides, named by NSID.",
        "required": ["nsid"],
        "properties": {
          "nsid": {
            "type": "string",
            "format": "nsid",
            "description": "NSID of the XRPC query or procedure the host serves for this session, e.g. 'com.atproto.repo.createRecord'."
          }
        }
      }
    }
  }

The capabilities endpoint allows different game hosts to provide different baseline capabilities while relying on lexicons to describe each capability in a portable way. It's up to the game host to handle the mapping of launch intent to grant to authorized capabilities, however the expectation is that these are all fired within a short period of time. It is the responsibility of the game to enumerate capabilities and establish the secure session with the game host quickly before tokens are expired.

First Party Play

While this system works well for a system like Cartridge many games way want to also provide a "first party launch" option. One option in doing so is allowing the game itself to perform and OAuth flow. However it is possible to also build a small service that implements the game host service flow that can be run in a first party context. Additionally this service can also act as a launch service proxy. When a request to launch a game is sent without a host service the proxy intercepts the request and injects itself as a host service, rewriting and resigning the response from the game itself. When the game is opened the user would go through an OAuth flow for the host proxy before being routed to the game itself. While this adds complexity it means that a single reusable component can be easily deployed by games, and unifies the api that a game must implement for things like save.

Privilege Escalation / Confirmation

There are many actions that a game may wish to take that could be destructive or broadly impactful. In general these should require user confirmation. The structure of the launch intent and game hosts is such that a game can be launched within an iframe that sits on a page owned by the game host. This allows certain capabilities to be offered via an escalation flow. In these cases the game would make a request to the game host to take an action, such as update an actor.rpg.sprite record. The game host would provide a defer response and present the user with it's own confirmation dialog. The styling of this dialog should make it clear that the confirmation is coming from the game host - helping to mitigate a diffusion of trust. Once the user has confirmed the action is ok, the game will be told it can make the request again.

We can model this as both an escalation path and a confirmation path. In the confirmation path the game attempts an action and the user is presented with a "allow/deny" option. The game must wait for the user to respond. In a escalation path the game may pre-ask for authorization at which point the user may allow/deny each request. This second flow allows the game to increase capabilities up-front not blocking users during game play.

In addition we can model capabilities based on OAuth client metadata. A game record itself could include a list of "requested capabilities", these would then be presented to the user in plain english on a games listing page before the game was launched. In practice users could even configure which of these permissions to disable by default before launching a game.

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions