# @indiegems/gem-web-sdk/server — API reference

Generated from `@indiegems/gem-web-sdk/server` v3.1.2. Every symbol below links to its page on the Gem Arcade developer site.

```js
import { defineServer } from '@indiegems/gem-web-sdk/server';
```

Install it by URL. It is a public download and needs no account and no credential:

```json
"devDependencies": {
  "@indiegems/gem-web-sdk": "https://downloads.gemarcade.com/sdk/v3.1.2/gem-web-sdk.tgz"
}
```

npm and pnpm both take a tarball URL and both write its hash into your lockfile, so every install after the first either gets the same bytes or fails. To upgrade, edit the version in the URL and install again.

## Base64String

[Base64String](https://gemarcade.com/developer/api/server#base64string)

```ts
type Base64String = string & { __contentEncoding: "base64" }
```

A base64-encoded value — a `string`, but not one you can substitute an
ordinary string for.

Some platform fields carry BYTES inside a JSON string, encoded as base64. An
entity setting's `value` is the one you are most likely to meet. Passing
ordinary text there fails the whole request, and it fails at runtime, on the
server, after you shipped.

This type moves that failure to your build. `{ value: JSON.stringify(state) }`
type-checks perfectly against `string` and is exactly the mistake being
stopped.

MAKE ONE WITH [toBase64](https://gemarcade.com/developer/api/server#tobase64). Writing `as Base64String` instead silences the
error and puts the trap back.

## toBase64

[toBase64](https://gemarcade.com/developer/api/server#tobase64)

```ts
function toBase64(value: string | Uint8Array<ArrayBufferLike>): Base64String
```

Encode text or bytes for a field that wants base64.

The only way to produce a [Base64String](https://gemarcade.com/developer/api/server#base64string), and the reason not to reach
for `btoa` yourself: `btoa` takes a *binary* string and throws on any
character above U+00FF, so `btoa(name)` works all through your testing and
then fails on the first player whose display name is not Latin-1. This
encodes a string as UTF-8 first.

| Parameter | Type | Description |
| --- | --- | --- |
| `value` | `string \| Uint8Array<ArrayBufferLike>` | Text, or bytes you already have. |

**Returns.** The encoded value, typed so the field will accept it.

```js
await gem.api.putEntityGameSetting('loadout', {
  value: toBase64(JSON.stringify(loadout)),
});
```

## GemErrorReason

[GemErrorReason](https://gemarcade.com/developer/api/server#gemerrorreason)

```ts
type GemErrorReason = "invalid" | "forbidden" | "not_found" | "conflict" | "precondition_failed" | "not_modified" | "insufficient_funds" | "too_large" | "rate_limited" | "unavailable"
```

What went wrong, in terms you can act on.

BRANCH ON THIS, not on the HTTP status — one status covers several distinct
refusals, and the reason is what tells them apart. It is carried by
[GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason).

## GemApiError

[GemApiError](https://gemarcade.com/developer/api/server#gemapierror)

```ts
class GemApiError
```

A refusal from the Gem Arcade backend. Any call that reaches the platform can
throw one.

You catch this; you never construct it. Branch on [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason)
for what went wrong and [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) for whether another
attempt could help — not on [GemApiError.status](https://gemarcade.com/developer/api/server#gemapierror-status), because one status
covers several distinct refusals.

```js
try {
  await somePlatformCall();
} catch (err) {
  if (err instanceof GemApiError && err.retryable) {
    const wait = err.retryAfterSeconds ?? 1;
    // try again after `wait` seconds
  } else {
    throw err;
  }
}
```

## GemApiError.reason

[GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason)

```ts
readonly reason: GemErrorReason
```

What went wrong, in terms you can act on — `invalid`, `forbidden`,
`not_found`, `conflict`, `precondition_failed`, `not_modified`,
`insufficient_funds`, `too_large`, `rate_limited` or `unavailable`.

This is the field to branch on. Two refusals can share a status and mean
different things; they do not share a reason.

## GemApiError.status

[GemApiError.status](https://gemarcade.com/developer/api/server#gemapierror-status)

```ts
readonly status: number
```

The HTTP status the server answered with.

Carried for logs and bug reports. Prefer [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason) for
decisions: several distinct refusals map onto one status, so branching
here treats different problems as the same one.

## GemApiError.detail

[GemApiError.detail](https://gemarcade.com/developer/api/server#gemapierror-detail)

```ts
readonly detail: string
```

The server's own sentence, without the `reason (status):` prefix that
`message` carries.

Use this when you want to quote the platform — in a log line, or a bug
report. It is not written for players, so do not put it on screen.

## GemApiError.retryAfterSeconds

[GemApiError.retryAfterSeconds](https://gemarcade.com/developer/api/server#gemapierror-retryafterseconds)

```ts
readonly retryAfterSeconds?: number
```

How long the server asked you to wait, in whole seconds.

ABSENT MEANS "NO ESTIMATE GIVEN", NEVER "DO NOT RETRY" —
[GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable) is what answers that. A refusal can be
worth repeating and still name no wait, so treat absence as "pick your
own backoff" rather than as a stop signal. The platform estimates a wait
for capacity and for rate limits, and sends none for the refusals it
cannot put a number on.

## GemApiError.code

[GemApiError.code](https://gemarcade.com/developer/api/server#gemapierror-code)

```ts
readonly code?: string
```

The platform's own error code, when it sent one —
`too_many_concurrent_matches`, `cluster_target_required`,
`no_compatible_server`.

Finer-grained than [GemApiError.reason](https://gemarcade.com/developer/api/server#gemapierror-reason), and the only way to tell
apart two refusals that share both a reason and a status. Absent on most
refusals, so treat it as extra information rather than something to
depend on.

## GemApiError.retryable

[GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable)

```ts
get retryable(): boolean
```

Whether another attempt could plausibly succeed.

True for `unavailable` and `rate_limited` and false for everything else —
a `forbidden` or `not_found` will refuse identically however many times you
send it. Check this before retrying, and wait
[GemApiError.retryAfterSeconds](https://gemarcade.com/developer/api/server#gemapierror-retryafterseconds) if the server named one.

## ClientOptions

[ClientOptions](https://gemarcade.com/developer/api/server#clientoptions)

```ts
interface ClientOptions
```

How a [GemClient](https://gemarcade.com/developer/api/server#gemclient) is built.

You rarely construct one yourself — [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it — but the
type has to be nameable to write a helper that takes one.

## ClientOptions.transport

[ClientOptions.transport](https://gemarcade.com/developer/api/server#clientoptions-transport)

```ts
readonly transport: Transport
```

How requests actually leave. Substitute one to test without a network — see
[Transport](https://gemarcade.com/developer/api/server#transport).

## ClientOptions.clock

[ClientOptions.clock](https://gemarcade.com/developer/api/server#clientoptions-clock)

```ts
readonly clock: Clock
```

Where time comes from, for retry backoff. See [Clock](https://gemarcade.com/developer/api/server#clock).

## ClientOptions.maxRetries

[ClientOptions.maxRetries](https://gemarcade.com/developer/api/server#clientoptions-maxretries)

```ts
readonly maxRetries?: number
```

How many times to retry a read that failed. Defaults to 2 — three attempts
in all.

ONLY READS ARE RETRIED, whatever you set. A write that timed out may already
have been applied, and repeating it is how a client charges someone twice.
Whether your write is safe to repeat is something only you know, so the SDK
does not guess.

## RequestOptions

[RequestOptions](https://gemarcade.com/developer/api/server#requestoptions)

```ts
interface RequestOptions
```

Per-call options, for the occasional call that needs one.

## RequestOptions.keepalive

[RequestOptions.keepalive](https://gemarcade.com/developer/api/server#requestoptions-keepalive)

```ts
readonly keepalive?: boolean
```

Let the request finish even if the page goes away.

For the last write before a player closes the tab. Browsers cap how much
can be in flight this way, so use it for the write that matters, not for
every write.

## RequestOptions.ifMatch

[RequestOptions.ifMatch](https://gemarcade.com/developer/api/server#requestoptions-ifmatch)

```ts
readonly ifMatch?: string
```

Only apply this write if nothing has changed since you read.

Pass the version that came back with the read your write is based on. If
someone else wrote in between, the call fails with `precondition_failed`
instead of overwriting them — re-read, decide again against the new state,
and write again.

## VersionedGetOptions

[VersionedGetOptions](https://gemarcade.com/developer/api/server#versionedgetoptions)

```ts
interface VersionedGetOptions
```

Options for a read that also wants the value's version back.

## VersionedGetOptions.signal

[VersionedGetOptions.signal](https://gemarcade.com/developer/api/server#versionedgetoptions-signal)

```ts
readonly signal?: AbortSignal
```

Cancel the read. An aborted call rejects rather than resolving.

## VersionedGetOptions.ifNoneMatch

[VersionedGetOptions.ifNoneMatch](https://gemarcade.com/developer/api/server#versionedgetoptions-ifnonematch)

```ts
readonly ifNoneMatch?: string
```

Ask "has this changed?" instead of "give me this".

Pass the version from your last read. If nothing has changed you get back
`{ modified: false }` and no body — cheaper for you and for the platform.

## VersionedResponse

[VersionedResponse](https://gemarcade.com/developer/api/server#versionedresponse)

```ts
type VersionedResponse<T> = { modified: true; body: T; etag?: string } | { modified: false; etag?: string }
```

What a versioned read answers: either the value and its version, or "nothing
changed".

CHECK `modified` FIRST. There is no `body` on the unchanged branch — not an
empty one, none — so the type will not let you read it until you have asked.
That is deliberate: an optional `body` would let an unhandled 304 through to
production as `undefined`.

```js
const result = await gem.client.getWithEtag(path, { ifNoneMatch: lastEtag });
if (result.modified) {
  render(result.body);
  lastEtag = result.etag;
}
```

## GemClient

[GemClient](https://gemarcade.com/developer/api/server#gemclient)

```ts
class GemClient
```

The HTTP client underneath the generated operations — for calls those do not
cover.

PREFER [ServerApi](https://gemarcade.com/developer/api/server#serverapi). Its methods are typed against the platform's own
description, so a shape that changes becomes a build error in your bundle.
This one takes a path and checks nothing you give it.

What it does give you is everything around the call: the credential, retries
on the requests that are safe to retry, and failures already classified into
[GemApiError](https://gemarcade.com/developer/api/server#gemapierror) so you can branch on a reason instead of a status.

ONLY READS ARE RETRIED. A write that timed out may well have been applied,
and retrying it silently is how a client duplicates a purchase it cannot see.
Whether your write is safe to repeat is something only you know.

You reach one as `gem.client`; there is normally no reason to construct it.

## GemClient.constructor

[GemClient.constructor](https://gemarcade.com/developer/api/server#gemclient-constructor)

```ts
constructor(options: ClientOptions): GemClient
```

Build a client. You normally do not — `createGemServer` builds one and hands
it to you as `gem.client`.

| Parameter | Type | Description |
| --- | --- | --- |
| `options` | `ClientOptions` | See [ClientOptions](https://gemarcade.com/developer/api/server#clientoptions). |

## GemClient.get

[GemClient.get](https://gemarcade.com/developer/api/server#gemclient-get)

```ts
get<T>(path: string, signal?: AbortSignal): Promise<T>
```

Read something.

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root, e.g. `/v1/players/me/settings`. |
| `signal` | `AbortSignal` | Cancels the read. |

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal — branch on `reason`.

## GemClient.getWithEtag

[GemClient.getWithEtag](https://gemarcade.com/developer/api/server#gemclient-getwithetag)

```ts
getWithEtag<T>(path: string, options?: VersionedGetOptions): Promise<VersionedResponse<T>>
```

Read something AND get its version back — or find out it has not changed.

The read half of optimistic concurrency: keep the version this returns, pass
it as `ifMatch` on your write, and the write is refused rather than
overwriting somebody. Pass it as `ifNoneMatch` here and an unchanged
resource comes back with no body at all.

SEPARATE FROM [GemClient.get](https://gemarcade.com/developer/api/server#gemclient-get) on purpose — `get` answers with the value
and nothing else, which is what most calls want.

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root. |
| `options` | `VersionedGetOptions` | See [VersionedGetOptions](https://gemarcade.com/developer/api/server#versionedgetoptions). |

**Returns.** See [VersionedResponse](https://gemarcade.com/developer/api/server#versionedresponse) — check `modified` before reading a body.

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.

## GemClient.put

[GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put)

```ts
put<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
```

Replace something.

NOT RETRIED. A write that timed out may already have been applied.

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root. |
| `body` | `unknown` | What to send. Encoded as JSON. |
| `signal` | `AbortSignal` |  |
| `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions) — `ifMatch` is how you avoid
  overwriting someone else's write. |

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.

## GemClient.post

[GemClient.post](https://gemarcade.com/developer/api/server#gemclient-post)

```ts
post<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
```

Create something, or perform an action.

NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put).

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root. |
| `body` | `unknown` | What to send. Encoded as JSON. |
| `signal` | `AbortSignal` |  |
| `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.

## GemClient.patch

[GemClient.patch](https://gemarcade.com/developer/api/server#gemclient-patch)

```ts
patch<T>(path: string, body: unknown, signal?: AbortSignal, options?: RequestOptions): Promise<T>
```

Change part of something.

NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put).

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root. |
| `body` | `unknown` | The change. Encoded as JSON. |
| `signal` | `AbortSignal` |  |
| `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.

## GemClient.delete

[GemClient.delete](https://gemarcade.com/developer/api/server#gemclient-delete)

```ts
delete<T>(path: string, signal?: AbortSignal, options?: RequestOptions): Promise<T>
```

Remove something.

NOT RETRIED, for the same reason as [GemClient.put](https://gemarcade.com/developer/api/server#gemclient-put).

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | `string` | From the API root. |
| `signal` | `AbortSignal` | Cancels the call. |
| `options` | `RequestOptions` | See [RequestOptions](https://gemarcade.com/developer/api/server#requestoptions). |

**Throws.** [GemApiError](https://gemarcade.com/developer/api/server#gemapierror) on a refusal.

## AuthFailure

[AuthFailure](https://gemarcade.com/developer/api/server#authfailure)

```ts
type AuthFailure = "chain_exhausted" | "unavailable" | "timeout" | "internal"
```

Why the session could not produce a usable token.

THIS IS THE CAUSE, NOT A VERDICT. It deliberately never distinguishes a
sign-out from a ban from a revocation, and it does not tell you whether to
try again — [AuthState](https://gemarcade.com/developer/api/server#authstate) does that. A game that branches on the reason
is guessing.

- `chain_exhausted` — the token chain cannot be renewed any further.
- `unavailable` — no token right now.
- `timeout` — the arcade did not answer in time.
- `internal` — something went wrong on this side.

## AuthState

[AuthState](https://gemarcade.com/developer/api/server#authstate)

```ts
type AuthState = "ready" | "pending" | "degraded" | "lost"
```

What the session can currently do, delivered to `GemOptions.onAuthStateChange`.

THIS IS THE ONE TO BRANCH ON, and the only route to it: the token source
itself is internal, so a game cannot read this state by asking for it. Watch
the callback instead.

- `ready` — a usable token is held.
- `pending` — no token yet, or one is being fetched.
- `degraded` — the last attempt failed but another may succeed. Keep playing;
  a retry is worthwhile.
- `lost` — terminal for this session. Signing back in is the only way
  forward, and retrying will not produce one.

## TokenGrant

[TokenGrant](https://gemarcade.com/developer/api/server#tokengrant)

```ts
interface TokenGrant
```

A credential and the two deadlines on it.

YOU DO NOT HANDLE ONE IN NORMAL USE. The SDK obtains it, refreshes it before
it expires, and attaches it to every call. It is named here because the
token-source signatures a bundle reads when wiring things up itself mention
it.

TWO DEADLINES, AND THEY MEAN DIFFERENT THINGS. `expiresAt` is when this
credential stops working and a fresh one is fetched — routine, invisible.
`chainEndsAt` is when fetching a fresh one stops being possible at all: past
it the session is over and only signing in again helps.

## TokenGrant.token

[TokenGrant.token](https://gemarcade.com/developer/api/server#tokengrant-token)

```ts
readonly token: string
```

The credential itself.

NEVER LOG IT AND NEVER FORWARD IT. Anything holding it can act as this
session until it expires.

## TokenGrant.expiresAt

[TokenGrant.expiresAt](https://gemarcade.com/developer/api/server#tokengrant-expiresat)

```ts
readonly expiresAt: number
```

When this credential stops working, in seconds since the Unix epoch.

Routine — the SDK renews before this, and you do not see it happen.

## TokenGrant.chainEndsAt

[TokenGrant.chainEndsAt](https://gemarcade.com/developer/api/server#tokengrant-chainendsat)

```ts
readonly chainEndsAt: number
```

When renewal stops being possible, in seconds since the Unix epoch.

Past this the session cannot be extended by anything. Watch
[AuthState](https://gemarcade.com/developer/api/server#authstate) rather than this number.

## AuthError

[AuthError](https://gemarcade.com/developer/api/server#autherror)

```ts
class AuthError
```

The session cannot produce a usable token.

Thrown by any call that needs the player's session. You catch this; you never
construct it.

IT CARRIES NO `retryable` FIELD, and that is deliberate rather than an
oversight — [AuthError.reason](https://gemarcade.com/developer/api/server#autherror-reason) is the cause, and the retry verdict
lives on the session's state. Watch `GemOptions.onAuthStateChange` and treat
[AuthState](https://gemarcade.com/developer/api/server#authstate) `degraded` as worth retrying and `lost` as terminal. A game
that decides from the reason will retry a session that can never come back.

## AuthError.reason

[AuthError.reason](https://gemarcade.com/developer/api/server#autherror-reason)

```ts
readonly reason: AuthFailure
```

Why the token could not be produced. See [AuthFailure](https://gemarcade.com/developer/api/server#authfailure) — it is the
cause, and not a signal about whether to try again.

## AuthError.isAuthError

[AuthError.isAuthError](https://gemarcade.com/developer/api/server#autherror-isautherror)

```ts
isAuthError(error: unknown): error is AuthError
```

Whether a caught value is an `AuthError`.

Prefer this to `instanceof`. Two copies of the SDK can coexist on a page —
the arcade vendors one build and games install another — and an error from
one copy fails `instanceof` in the other. This check does not care which
copy minted it.

```js
try {
  await somePlatformCall();
} catch (err) {
  if (AuthError.isAuthError(err)) {
    // the session is the problem; watch onAuthStateChange for whether it recovers
  }
}
```

The call is left unnamed on purpose: this error reaches a game and a server
bundle alike, and the two have no call in common.

| Parameter | Type |
| --- | --- |
| `error` | `unknown` |

## TransportRefusal

[TransportRefusal](https://gemarcade.com/developer/api/server#transportrefusal)

```ts
class TransportRefusal
```

The request was never sent, because sending it would have been wrong.

ALWAYS YOUR BUG, never a transient fault, and never worth retrying — the same
call will be refused identically every time. Six things produce one: a path
or base URL that will not parse, a path that changes the protocol or host, a
path or base URL carrying credentials, a body that will not serialise to
JSON, a conditional-header value the wire cannot carry, and a retry count
outside 0..10.

Nothing reached the platform, so there is no status and no response to read.
Fix the call.

## TransportRefusal.retryable

[TransportRefusal.retryable](https://gemarcade.com/developer/api/server#transportrefusal-retryable)

```ts
readonly retryable: false
```

Always `false`.

Present so that code branching on `retryable` across the errors this SDK
throws reads a real `false` here rather than `undefined`. A refusal is
deterministic; retrying cannot change it.

## TransportRefusal.isRefusal

[TransportRefusal.isRefusal](https://gemarcade.com/developer/api/server#transportrefusal-isrefusal)

```ts
isRefusal(error: unknown): error is TransportRefusal
```

Whether a caught value is a `TransportRefusal`.

Prefer this to `instanceof`. Two copies of the SDK can coexist on a page —
the arcade vendors one build and games install another — and an error from
one copy fails `instanceof` in the other.

| Parameter | Type |
| --- | --- |
| `error` | `unknown` |

## CONDITIONAL_HEADERS

[CONDITIONAL_HEADERS](https://gemarcade.com/developer/api/server#conditional_headers)

```ts
CONDITIONAL_HEADERS: readonly ["if-match", "if-none-match"]
```

The only request headers a call may set: `if-match` and `if-none-match`.

A CLOSED SET, ON PURPOSE. Everything else — the credential, the content type,
the client's identity — is the SDK's to send, and a call that could add its
own headers could send headers the platform trusts.

## ConditionalRequestHeaders

[ConditionalRequestHeaders](https://gemarcade.com/developer/api/server#conditionalrequestheaders)

```ts
type ConditionalRequestHeaders = Partial<Record<unknown[number], string>>
```

The type of [TransportRequest.headers](https://gemarcade.com/developer/api/server#transportrequest-headers) — derived from
[CONDITIONAL_HEADERS](https://gemarcade.com/developer/api/server#conditional_headers), so the two cannot disagree.

## TransportRequest

[TransportRequest](https://gemarcade.com/developer/api/server#transportrequest)

```ts
interface TransportRequest
```

One outgoing call, as a [Transport](https://gemarcade.com/developer/api/server#transport) receives it.

You meet this when you write a transport — usually in a test, to assert what
your code sends without a network.

## TransportRequest.method

[TransportRequest.method](https://gemarcade.com/developer/api/server#transportrequest-method)

```ts
readonly method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE"
```

The HTTP method.

## TransportRequest.path

[TransportRequest.path](https://gemarcade.com/developer/api/server#transportrequest-path)

```ts
readonly path: string
```

The path, from the API root — `/v1/players/me/settings`. Never a full URL:
where it is sent is not a call's decision.

## TransportRequest.body

[TransportRequest.body](https://gemarcade.com/developer/api/server#transportrequest-body)

```ts
readonly body?: unknown
```

The request body, if there is one. Encoded as JSON.

## TransportRequest.headers

[TransportRequest.headers](https://gemarcade.com/developer/api/server#transportrequest-headers)

```ts
readonly headers?: Partial<Record<"if-match" | "if-none-match", string>>
```

See [ConditionalRequestHeaders](https://gemarcade.com/developer/api/server#conditionalrequestheaders) — nothing else may be set.

## TransportRequest.signal

[TransportRequest.signal](https://gemarcade.com/developer/api/server#transportrequest-signal)

```ts
readonly signal?: AbortSignal
```

Cancels the request. Timeouts are the caller's policy, not the SDK's.

## TransportRequest.keepalive

[TransportRequest.keepalive](https://gemarcade.com/developer/api/server#transportrequest-keepalive)

```ts
readonly keepalive?: boolean
```

Let the request outlive the page.

For the write fired as a player leaves. Without this, a request started
while the page is unloading is simply cancelled — it looks like it was sent,
and nothing arrives.

NOT ON EVERYTHING, because browsers cap how much can be in flight this way
(64 KB across all such requests). Use it for the write that matters.

## TransportResponse

[TransportResponse](https://gemarcade.com/developer/api/server#transportresponse)

```ts
interface TransportResponse
```

What a [Transport](https://gemarcade.com/developer/api/server#transport) answers with.

TWO HEADERS ARE PARSED OUT and there is no header bag, because these are the
only two anything above this point reads — which also keeps a stub in a test
to a couple of fields.

## TransportResponse.status

[TransportResponse.status](https://gemarcade.com/developer/api/server#transportresponse-status)

```ts
readonly status: number
```

The HTTP status.

## TransportResponse.body

[TransportResponse.body](https://gemarcade.com/developer/api/server#transportresponse-body)

```ts
readonly body: unknown
```

The decoded response body.

## TransportResponse.etag

[TransportResponse.etag](https://gemarcade.com/developer/api/server#transportresponse-etag)

```ts
readonly etag?: string
```

The resource's version, parsed out of the response.

Absent when the server sent none — not every resource is versioned. If you
are writing a transport for a test, set it: leaving it out makes every
conditional read look unversioned.

## TransportResponse.retryAfterSeconds

[TransportResponse.retryAfterSeconds](https://gemarcade.com/developer/api/server#transportresponse-retryafterseconds)

```ts
readonly retryAfterSeconds?: number
```

How long the server asked you to wait, in WHOLE SECONDS.

Seconds, not milliseconds — do not divide it by anything.

ABSENT MEANS "NO ESTIMATE", NEVER "DO NOT RETRY". Whether a refusal is
worth repeating is [GemApiError.retryable](https://gemarcade.com/developer/api/server#gemapierror-retryable)'s answer, not this field's.

## Transport

[Transport](https://gemarcade.com/developer/api/server#transport)

```ts
interface Transport
```

How calls actually leave — the seam you replace to test without a network.

The SDK builds a real one for you. Pass your own when you want to assert what
your code sends, or to answer it without a server.

```js
const sent = [];
const transport = {
  async request(req) {
    sent.push(req);
    return { status: 200, body: { ok: true } };
  },
};
```

## Transport.request

[Transport.request](https://gemarcade.com/developer/api/server#transport-request)

```ts
request(request: TransportRequest): Promise<TransportResponse>
```

Send one request and resolve with what came back.

| Parameter | Type |
| --- | --- |
| `request` | `TransportRequest` |

## Clock

[Clock](https://gemarcade.com/developer/api/server#clock)

```ts
interface Clock
```

Where time comes from — so a test does not have to wait for it.

Credential refresh, retry backoff and timeouts are all wall-clock behaviour.
A test that has to wait ten real seconds to prove a timeout fires is a test
nobody runs, so every SDK option that involves time takes one of these.

Pass [systemClock](https://gemarcade.com/developer/api/server#systemclock) — or nothing, which means the same — to use real
time. Pass your own to control it.

```js
let now = 0;
const timers = [];
const clock = {
  now: () => now,
  setTimeout: (fn, ms) => timers.push({ fn, at: now + ms }),
  clearTimeout: () => {},
};
// advance time yourself, and run whatever is due
```

## Clock.now

[Clock.now](https://gemarcade.com/developer/api/server#clock-now)

```ts
now(): number
```

The current time, in milliseconds — the same scale as `Date.now()`.

## Clock.setTimeout

[Clock.setTimeout](https://gemarcade.com/developer/api/server#clock-settimeout)

```ts
setTimeout(fn: () => void, ms: number): unknown
```

Run `fn` after `ms` milliseconds. Return whatever identifies it; the SDK
only ever hands that value back to [Clock.clearTimeout](https://gemarcade.com/developer/api/server#clock-cleartimeout).

| Parameter | Type |
| --- | --- |
| `fn` | `() => void` |
| `ms` | `number` |

## Clock.clearTimeout

[Clock.clearTimeout](https://gemarcade.com/developer/api/server#clock-cleartimeout)

```ts
clearTimeout(handle: unknown): void
```

Cancel a timer created by [Clock.setTimeout](https://gemarcade.com/developer/api/server#clock-settimeout).

| Parameter | Type |
| --- | --- |
| `handle` | `unknown` |

## systemClock

[systemClock](https://gemarcade.com/developer/api/server#systemclock)

```ts
systemClock: Clock
```

Real time — the [Clock](https://gemarcade.com/developer/api/server#clock) every SDK option defaults to.

Name it when you want to be explicit, or wrap it when you want to watch what
the SDK schedules without replacing the clock entirely.

## defineServer

[defineServer](https://gemarcade.com/developer/api/server#defineserver)

```ts
function defineServer(setup: (gem: GemMatchServer) => GemServerBundle, options?: GemServerOptions): GemServerFactory
```

Declare your dedicated server. Default-export the result.

You pass a setup function; it receives a [GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver) — the platform
operations, your room, your players — and returns the handlers you want. The
credential and its refresh are handled for you, and the SDK is disposed when
the match ends.

WHY A FUNCTION RATHER THAN A PLAIN OBJECT: your handlers need to share state.
The closure the setup function creates is where your world lives, alongside
`gem`, without threading either through five signatures.

A THROWN ERROR IS NOT SWALLOWED. The platform logs it and keeps serving —
except in `init`, where it fails the match. That is deliberate: a server with
a broken handler quietly serving nobody is worse than one that stops.

| Parameter | Type | Description |
| --- | --- | --- |
| `setup` | `(gem: GemMatchServer) => GemServerBundle` | Called once. Return the handlers you want from
  [GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle); omit the rest. |
| `options` | `GemServerOptions` | Passed through to [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Normally omit it. |

**Returns.** The value to default-export from your bundle's entry file.

```js
import { defineServer } from '@indiegems/gem-web-sdk/server';

export default defineServer((gem) => {
  let round = 0;
  return {
    async init() {
      const room = await gem.room?.get();
      gem.log(`room has ${room?.members.length ?? 0} player(s)`);
    },
    message(playerId, data, isText) {
      if (isText && data === 'next') gem.broadcast(`round ${++round}`);
    },
    async shutdown() {
      await gem.match.end();
    },
  };
});
```

**Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)

## createMatchServer

[createMatchServer](https://gemarcade.com/developer/api/server#creatematchserver)

```ts
function createMatchServer(host: GemServerHost, options?: GemServerOptions): GemMatchServer
```

Build the `gem` object yourself, when you are driving the lifecycle by hand.

MOST BUNDLES WANT [defineServer](https://gemarcade.com/developer/api/server#defineserver), which calls this for you and wires up
your handlers. Use this one if you are writing the lifecycle object directly.

CALL IT FROM `init`, not at module scope — it needs the host, and it builds
the platform connection immediately, which is what turns "this platform image
is too old" into a startup failure rather than a surprise mid-match. Call
`dispose()` from `shutdown`.

| Parameter | Type | Description |
| --- | --- | --- |
| `host` | `GemServerHost` | The object the runtime passed to `init`. |
| `options` | `GemServerOptions` | Passed through to [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Normally omit it. |

**Throws.** When the runtime named no API host, so no call could succeed.

```js
import { createMatchServer } from '@indiegems/gem-web-sdk/server';

export default () => {
  let gem;
  return {
    async init(host) { gem = createMatchServer(host); },
    join(playerId) { gem.broadcast(`${playerId} joined`); },
    async shutdown() { await gem.match.end(); gem.dispose(); },
  };
};
```

## createGemServer

[createGemServer](https://gemarcade.com/developer/api/server#creategemserver)

```ts
function createGemServer(host: ServerBundleHost, options?: GemServerOptions): GemServer
```

Build your server's connection to the platform. Call it once, in `init(host)`.

Pass the `host` object the runtime handed you and you get back a
[GemServer](https://gemarcade.com/developer/api/server#gemserver): the platform operations, the room reads for your own room,
and a way to end the match. The credential is taken from the host and kept
fresh for you.

MOST BUNDLES DO NOT CALL THIS. [defineServer](https://gemarcade.com/developer/api/server#defineserver) does it for you and hands
you the result. Reach for it directly when you are managing the lifecycle
yourself.

IT THROWS AT STARTUP if the runtime did not name an API to call — an old
platform image. That is deliberate: a server that silently cannot reach the
platform is worse than one that refuses to start.

| Parameter | Type | Description |
| --- | --- | --- |
| `host` | `ServerBundleHost` | The object the runtime passes to `init`. |
| `options` | `GemServerOptions` | See [GemServerOptions](https://gemarcade.com/developer/api/server#gemserveroptions) — normally omit it. |

**Throws.** When the runtime named no API host, so no call could succeed.

```js
import { createGemServer } from '@indiegems/gem-web-sdk/server';

export async function init(host) {
  const gem = createGemServer(host);
  const room = await gem.room?.get();
  host.log(`match ${gem.identity.matchId} with ${room?.members.length ?? 0}`);
}
```

**Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)

## createHostTokenSource

[createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource)

```ts
function createHostTokenSource(host: ServerBundleHost, options: HostTokenSourceOptions): HostTokenSource
```

Keep the platform credential fresh, without building the rest of the SDK.

MOST BUNDLES NEVER CALL THIS. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it, and hands you a
client that is already authenticated. Reach for it when you are assembling
the pieces yourself.

It subscribes to the host's credential pushes straight away. Call `dispose()`
when you are done, or a pending refresh timer will keep your process alive
after the match is over.

| Parameter | Type | Description |
| --- | --- | --- |
| `host` | `ServerBundleHost` | The object the runtime passed to `init`. |
| `options` | `HostTokenSourceOptions` | See [HostTokenSourceOptions](https://gemarcade.com/developer/api/server#hosttokensourceoptions) — `clock` is required. |

**Returns.** Something you can stop. See [HostTokenSource](https://gemarcade.com/developer/api/server#hosttokensource).

## GemMatchServer

[GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver)

```ts
interface GemMatchServer
```

The `gem` your bundle holds — everything a dedicated server can do, on one
object.

This is what [defineServer](https://gemarcade.com/developer/api/server#defineserver) hands your setup function. It is a
[GemServer](https://gemarcade.com/developer/api/server#gemserver) — the platform operations, your room, ending the match —
with the match surface added: the players connected right now, and how to talk
to them.

The allocation is spelled ONE way. There is no `gem.matchId`; it is
`gem.identity.matchId`.

```js
export default defineServer((gem) => ({
  join(playerId) {
    const who = gem.profile(playerId);
    gem.broadcast(`${playerId} joined (seat ${gem.playerNumber(playerId)})`);
    gem.log(`now ${gem.players().length} connected`);
  },
}));
```

## GemServerBundle

[GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle)

```ts
interface GemServerBundle
```

The five things that can happen to your server. Define the ones you care
about; each may be `async` and is awaited before the next event arrives.

This is what your setup function returns — see [defineServer](https://gemarcade.com/developer/api/server#defineserver). Events
are delivered one at a time, in order, so you never handle two at once and do
not need locking around your own state.

```js
export default defineServer((gem) => {
  const scores = new Map();
  return {
    join(playerId) { scores.set(playerId, 0); },
    leave(playerId) { scores.delete(playerId); },
    message(playerId, data, isText) {
      if (isText) scores.set(playerId, scores.get(playerId) + Number(data));
    },
    async shutdown() { await gem.match.end(); },
  };
});
```

## GemServerFactory

[GemServerFactory](https://gemarcade.com/developer/api/server#gemserverfactory)

```ts
type GemServerFactory = () => GemServerLifecycle
```

The type of your bundle's default export — what [defineServer](https://gemarcade.com/developer/api/server#defineserver) returns.

The platform calls it once, at the start of a match.

## GemServerLifecycle

[GemServerLifecycle](https://gemarcade.com/developer/api/server#gemserverlifecycle)

```ts
interface GemServerLifecycle
```

What the platform actually calls — a [GemServerBundle](https://gemarcade.com/developer/api/server#gemserverbundle) after the SDK has
wrapped it.

YOU DO NOT WRITE ONE. [defineServer](https://gemarcade.com/developer/api/server#defineserver) produces it from your bundle. It is
named here so you can type a variable holding the result, and so the shape the
platform drives is written down somewhere a reader can find it.

`init` and `shutdown` are required here although both are optional on your
bundle: the SDK always defines them, to receive the host and to release the
credential subscription when the match is over.

## GemServerProfile

[GemServerProfile](https://gemarcade.com/developer/api/server#gemserverprofile)

```ts
interface GemServerProfile
```

Who a player is, for display. Safe to hold; think before you relay it.

Returned by [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile).

RELAY THE ID, NOT THE NAME. Sending player ids to your clients and letting
each one render the name it already has is better than broadcasting names
through your server: it is less to send, and it keeps one player's text from
reaching every other player's screen through you.

## GemServer

[GemServer](https://gemarcade.com/developer/api/server#gemserver)

```ts
interface GemServer
```

Your server's connection to the platform — what [createGemServer](https://gemarcade.com/developer/api/server#creategemserver)
returns.

Build one in `init(host)`, keep it, and call through it for the life of the
match. It holds the credential and refreshes it for you; there is nothing to
sign, renew or attach.

```js
import { defineServer } from '@indiegems/gem-web-sdk/server';

export default defineServer((gem) => ({
  async init() {
    const room = await gem.room?.get();
    gem.log(`starting with ${room?.members.length ?? 0} player(s)`);
  },
  async shutdown() {
    await gem.match.end();
  },
}));
```

## GemServerIdentity

[GemServerIdentity](https://gemarcade.com/developer/api/server#gemserveridentity)

```ts
interface GemServerIdentity
```

Which game, channel and match this server was started for.

You never construct one. The platform decides all of it when it allocates the
server, and [GemServer.identity](https://gemarcade.com/developer/api/server#gemserver-identity) is it restated in one place so you do
not have to keep reaching back to the host object.

These are also the ids bound into every call `api` makes, which is why a
bundle never types a game id and cannot name another game's.

## GemServerOptions

[GemServerOptions](https://gemarcade.com/developer/api/server#gemserveroptions)

```ts
interface GemServerOptions
```

Options for [createGemServer](https://gemarcade.com/developer/api/server#creategemserver). Every one is optional, and a bundle
running on the platform normally passes none.

They exist for the two cases the platform does not cover: pointing a bundle at
something other than the API the platform named, and running it in a test.

## ServerApi

[ServerApi](https://gemarcade.com/developer/api/server#serverapi)

```ts
type ServerApi = unknown
```

Every platform operation your server can call, already holding its credential.

This is the surface you spend most of your time in: `gem.api.getGameRoomV2(id)`,
`gem.api.createOrder(...)`, and the rest. Each method's arguments and return
type come from the platform's own API description, so what you can pass and
what you get back are the contract rather than someone's reading of it.

`game_id` is never a parameter. It is filled in from the allocation — in the
path and the query alike — so a bundle cannot address another game's data even
by mistake.

The operation list moves with the platform. An operation added to your game's
API appears here without this package changing, and one that is withdrawn
stops type-checking rather than failing at runtime.

**Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles)

## ServerBundleHost

[ServerBundleHost](https://gemarcade.com/developer/api/server#serverbundlehost)

```ts
interface ServerBundleHost
```

The part of the host object that identifies the match and carries the
credential.

This is what [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) needs, and nothing more — which is also
what makes it easy to stand up in a test: four members instead of the whole
host. [GemServerHost](https://gemarcade.com/developer/api/server#gemserverhost) is the same object at full width.

## ServerTokenGrant

[ServerTokenGrant](https://gemarcade.com/developer/api/server#servertokengrant)

```ts
interface ServerTokenGrant
```

A credential the platform handed your server, and when it stops working.

You do not normally touch one. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) takes the credential
from the host and refreshes it for you; this type is here for a bundle wiring
the pieces up itself.

## HostTokenSource

[HostTokenSource](https://gemarcade.com/developer/api/server#hosttokensource)

```ts
interface HostTokenSource
```

A credential subscription — what [createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource) gives back.

Deliberately narrow: the only thing you can do with it is stop it. The
credential itself is not reachable, because nothing you can call takes one.

## GemServerHost

[GemServerHost](https://gemarcade.com/developer/api/server#gemserverhost)

```ts
interface GemServerHost
```

The whole `host` object, as the runtime passes it to your bundle.

[ServerBundleHost](https://gemarcade.com/developer/api/server#serverbundlehost) is its credential half; everything added here is the
match — who is connected, what they sent, and how to answer.

SEVERAL MEMBERS ARE OPTIONAL BECAUSE THE PLATFORM IMAGE MAY BE OLDER than the
member. There is no version to ask for; presence is the signal. Check before
calling, and your bundle keeps running on every image rather than only the
newest.

A bundle written with [defineServer](https://gemarcade.com/developer/api/server#defineserver) usually reads [GemMatchServer](https://gemarcade.com/developer/api/server#gemmatchserver)
instead, which wraps this.

## GemServerHostProfile

[GemServerHostProfile](https://gemarcade.com/developer/api/server#gemserverhostprofile)

```ts
interface GemServerHostProfile
```

A player's display identity, as the platform resolved it for your server.

EVERY FIELD IS OPTIONAL. The platform may not have all of them for a given
player, and that is not an error — fall back to something of your own.

[GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile) is the version you usually want: same
information, with the name typed as [PlayerText](https://gemarcade.com/developer/api/server#playertext).

## SendOptions

[SendOptions](https://gemarcade.com/developer/api/server#sendoptions)

```ts
interface SendOptions
```

What delivery you are ASKING FOR when you send.

A request, not a guarantee in either direction: a transport may deliver an
unreliable message reliably anyway, and it never does the reverse. What you
asked for is carried to the receiver either way, so they can tell the
difference.

## BroadcastOptions

[BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions)

```ts
interface BroadcastOptions
```

The same delivery choices as [SendOptions](https://gemarcade.com/developer/api/server#sendoptions), plus who to skip.

## HostTokenSourceOptions

[HostTokenSourceOptions](https://gemarcade.com/developer/api/server#hosttokensourceoptions)

```ts
interface HostTokenSourceOptions
```

Options for [createHostTokenSource](https://gemarcade.com/developer/api/server#createhosttokensource).

You need these only if you are wiring the credential up yourself.
[createGemServer](https://gemarcade.com/developer/api/server#creategemserver) does it for you.

## RoomArgValue

[RoomArgValue](https://gemarcade.com/developer/api/server#roomargvalue)

```ts
type RoomArgValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[]
```

What a room setting can be: a string, a number, a boolean, or an array of one
of those.

FLAT. There is no nested-object arm, because a room's settings schema cannot
declare one. What you get is what your game's published configuration said a
room may be created with, already type-checked by the platform before the
match was allocated — so a bundle reading settings does not need to validate
them again.

**Guides.** [rooms-and-matches](https://gemarcade.com/developer/docs/rooms-and-matches)

## GameApi

[GameApi](https://gemarcade.com/developer/api/server#gameapi)

```ts
interface GameApi
```

What a generated call needs in order to make itself: the client that sends
it, and the game it belongs to.

YOU DO NOT BUILD ONE. `createGemServer` does, and hands you the operations
already holding it as `gem.api`. It is named here because it appears in the
type of those operations, so a helper of your own that takes them has to be
able to spell it.

`gameId` is a call rather than a field because the game is not known until
the platform has issued a credential, which happens after the client exists.
It is never something a caller passes: a bundle cannot address another
game's data because it never types a game id, in a path or in a query.

## GameApi.client

[GameApi.client](https://gemarcade.com/developer/api/server#gameapi-client)

```ts
readonly client: GemClient
```

The client every operation sends through.

## GameApi.gameId

[GameApi.gameId](https://gemarcade.com/developer/api/server#gameapi-gameid)

```ts
gameId(): string
```

The game this server is running — taken from its credential.

## PlayerText

[PlayerText](https://gemarcade.com/developer/api/server#playertext)

```ts
class PlayerText
```

A player-authored string, which your game must convert before rendering.

Player names are chosen by players, and length limits are not content limits.
If your game assigned one straight to `innerHTML`, whoever picked that name
would be running code on your page — with your play token, which is the whole
session. So the SDK does not hand you a string: it hands you this, and the
unsafe thing does not compile.

There is exactly one way out, and it belongs at your model boundary:

```js
const { text } = window.GemWebSdkGame;

const name = text.toPlain(profile.displayName);
label.textContent = name;
```

To sort names, use [text.compare](https://gemarcade.com/developer/api/server#text-compare) rather than converting first — it is
locale-aware, which a plain `<` comparison is not.

## PlayerText.toString

[PlayerText.toString](https://gemarcade.com/developer/api/server#playertext-tostring)

```ts
toString(): string
```

A sentinel that names its own fix, never the player's text.

If you see `[gem: use gem.text.toPlain()]` on screen, something rendered
the wrapper instead of converting it. That is the gate working — the
alternative was shipping an unescaped player name.

⚠️ THE SENTINEL NAMES `gem.text`, WHICH DOES NOT EXIST. `text` is reached
from the SDK global — `const { text } = window.GemWebSdkGame;` — not from
your `gem` instance. Follow that rather than the message.

## PlayerText.toJSON

[PlayerText.toJSON](https://gemarcade.com/developer/api/server#playertext-tojson)

```ts
toJSON(): string
```

The same sentinel, so putting a player name into a save blob or a telemetry
payload cannot silently store unescaped text.

Convert with [text.toPlain](https://gemarcade.com/developer/api/server#text-toplain) before serialising anything you intend to
keep.

## text

[text](https://gemarcade.com/developer/api/server#text)

```ts
text: { toPlain: null; compare: null; equals: null }
```

The only ways out of [PlayerText](https://gemarcade.com/developer/api/server#playertext).

Reached from the SDK global rather than from `gem`:
`const { text } = window.GemWebSdkGame;`

Two calls:

- `toPlain(value)` — the plain string, safe for `textContent`, canvas
  `fillText` and interpolation. The one call you make, and it belongs at your
  model boundary rather than scattered through render code.
- `compare(a, b, locale?)` — locale-aware comparison for sorting rosters and
  leaderboards. Use it instead of converting and comparing with `<`, which
  sorts by code point and gets accented and non-Latin names wrong. It accepts
  plain strings too, because real leaderboards mix player names with rows
  your game authored, and it falls back rather than throwing on a bad locale
  tag — a throw inside a comparator takes the whole sort down.

```js
const { text } = window.GemWebSdkGame;

const name = text.toPlain(profile.displayName);
players.sort((a, b) => text.compare(a.displayName, b.displayName, locale));
```

THERE IS DELIBERATELY NO `toHtml`. Escaping depends on where the value
lands — safe in element text and quoted attributes, unsafe in URL, style and
script contexts — and your framework already escapes. One unambiguous
function beats two where one carries a caveat.

## text.toPlain

[text.toPlain](https://gemarcade.com/developer/api/server#text-toplain)

```ts
toPlain(t: PlayerText): string
```

The plain string, safe for `textContent`, canvas `fillText` and
interpolation.

The one call you make, and it belongs at your model boundary rather than
scattered through your render code:

```js
const name = text.toPlain(profile.displayName);
```

Safe for text; NOT pre-escaped for HTML. Do not put the result into
`innerHTML`, a URL, a style or a script — convert at the boundary and let
your framework escape, which is what it already does.

| Parameter | Type |
| --- | --- |
| `t` | `PlayerText` |

## text.compare

[text.compare](https://gemarcade.com/developer/api/server#text-compare)

```ts
compare(a: string | PlayerText, b: string | PlayerText, locale?: string): number
```

Locale-aware comparison, for sorting rosters and leaderboards.

Use this rather than converting to strings and comparing with `<`, which
sorts by code point and gets accented and non-Latin names wrong.

```js
players.sort((a, b) => text.compare(a.displayName, b.displayName, locale));
```

Accepts plain strings as well as player text, because real leaderboards mix
player names with rows your game authored. Pass the player's locale from
`GemPrefs.locale`; without one it uses the runtime's. A malformed
locale tag falls back rather than throwing — this is a comparator, and a
throw here would take the whole sort down with it.

| Parameter | Type |
| --- | --- |
| `a` | `string \| PlayerText` |
| `b` | `string \| PlayerText` |
| `locale` | `string` |

## text.equals

[text.equals](https://gemarcade.com/developer/api/server#text-equals)

```ts
equals(a: string | PlayerText, b: string | PlayerText): boolean
```

Whether two names are the same name, without unwrapping either.

Compares normalised forms, so two spellings of the same name — one
composed, one decomposed — come out equal. That matters because a name may
reach your game from somewhere other than the arcade.

**KEY YOUR ROWS ON THE PLAYER'S ID, NEVER ON A NAME.** Names are not
identifiers: two players can choose the same one, and a player can change
theirs. Use this for display-level comparisons, not for identity.

| Parameter | Type |
| --- | --- |
| `a` | `string \| PlayerText` |
| `b` | `string \| PlayerText` |

## GemServerOptions.baseUrl

[GemServerOptions.baseUrl](https://gemarcade.com/developer/api/server#gemserveroptions-baseurl)

```ts
readonly baseUrl?: string
```

Where to send API calls, INSTEAD of where the platform said.

The host already names it, and a bundle running for real should not
second-guess that. This is for a rig — a tunnel, an API running on your own
machine — not for choosing which environment you are in. Setting it in a
shipped bundle means your server talks to somewhere the platform did not
send it.

## GemServerOptions.clock

[GemServerOptions.clock](https://gemarcade.com/developer/api/server#gemserveroptions-clock)

```ts
readonly clock?: Clock
```

Where time comes from. Defaults to the real clock.

Pass a [Clock](https://gemarcade.com/developer/api/server#clock) in a test so token refresh and retry backoff happen when
you say rather than when a real second passes.

## GemServerOptions.fetch

[GemServerOptions.fetch](https://gemarcade.com/developer/api/server#gemserveroptions-fetch)

```ts
readonly fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
```

The `fetch` used for every call. Defaults to the runtime's own.

For tests: a stub here is how you assert what your bundle sends without a
network.

## GemServerOptions.onAuthStateChange

[GemServerOptions.onAuthStateChange](https://gemarcade.com/developer/api/server#gemserveroptions-onauthstatechange)

```ts
readonly onAuthStateChange?: (state: AuthState, reason?: AuthFailure) => void
```

Called whenever the server's ability to make authenticated calls changes.

The same signal the browser half delivers to a game, for the same reason:
the token is refreshed for you, and this is how you find out when that stops
working. See [AuthState](https://gemarcade.com/developer/api/server#authstate) for what to branch on and [AuthFailure](https://gemarcade.com/developer/api/server#authfailure)
for the cause.

## GemServerIdentity.gameId

[GemServerIdentity.gameId](https://gemarcade.com/developer/api/server#gemserveridentity-gameid)

```ts
readonly gameId: string
```

The game this server belongs to.

## GemServerIdentity.channel

[GemServerIdentity.channel](https://gemarcade.com/developer/api/server#gemserveridentity-channel)

```ts
readonly channel: string
```

The channel it was allocated on — `dev`, `test` or `release`.

## GemServerIdentity.matchId

[GemServerIdentity.matchId](https://gemarcade.com/developer/api/server#gemserveridentity-matchid)

```ts
readonly matchId: string
```

The match it is running.

## GemServerIdentity.gameRoomId

[GemServerIdentity.gameRoomId](https://gemarcade.com/developer/api/server#gemserveridentity-gameroomid)

```ts
readonly gameRoomId?: string
```

The room this match came from, when there is one.

CHECK IT BEFORE USING IT. It is absent on older platform images, which is
also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can be absent — the two go together.

## GemServer.identity

[GemServer.identity](https://gemarcade.com/developer/api/server#gemserver-identity)

```ts
readonly identity: GemServerIdentity
```

Which game, channel and match this server is running.

## GemServer.client

[GemServer.client](https://gemarcade.com/developer/api/server#gemserver-client)

```ts
readonly client: GemClient
```

The HTTP client underneath [GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api), for calls the generated
surface does not cover.

PREFER `api`. It is typed against the platform's description, so a shape
that changes becomes a build error in your bundle. This one takes a path and
checks nothing — reach for it when you genuinely need something `api` has
no method for, and treat that as worth telling us about.

## GemServer.api

[GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api)

```ts
readonly api: ServerApi
```

Every platform operation your server can call. This is the one you want.

See [ServerApi](https://gemarcade.com/developer/api/server#serverapi).

## GemServer.match

[GemServer.match](https://gemarcade.com/developer/api/server#gemserver-match)

```ts
readonly match: { end: null }
```

Ending the match.

`end()` is HOW A SERVER-AUTHORITATIVE GAME FINISHES. It tells the platform
the match is over; the platform then tells the runtime, and your server
winds down through its ordinary shutdown. You do not stop the process
yourself.

It always means THIS match — the id is already known, so there is nothing to
pass and no way to end someone else's.

## GemServer.match.end

[GemServer.match.end](https://gemarcade.com/developer/api/server#gemserver-match-end)

```ts
end(signal?: AbortSignal): Promise<void>
```

Tell the platform this match is finished.

| Parameter | Type | Description |
| --- | --- | --- |
| `signal` | `AbortSignal` | Cancels the call — not the match. |

## GemServer.room

[GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room)

```ts
readonly room?: { get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>; args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse> }
```

Reading the room this match came from — the two calls almost every server
makes at startup, already pointed at your own room.

CHECK IT IS THERE. `room` is absent on older platform images, which is why
the type says it might be. `gem.room?.get()` is the shape to write. When it
is absent the same reads are still available through `api` if you know the
room id from somewhere else.

## GemServer.room.get

[GemServer.room.get](https://gemarcade.com/developer/api/server#gemserver-room-get)

```ts
get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>
```

The room, in full: the settings it was created with, the template those
came from, and every member — with display name, role, ready state and
avatar.

This is what you want at `start()`, and it is a superset of what the older
roster push carried.

## GemServer.room.args

[GemServer.room.args](https://gemarcade.com/developer/api/server#gemserver-room-args)

```ts
args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse>
```

Just the room's settings.

A room whose template declares no settings answers 404 rather than an
empty object — "this room has no settings" and "this room's settings are
empty" are different answers and the API keeps them apart.

## GemServer.dispose

[GemServer.dispose](https://gemarcade.com/developer/api/server#gemserver-dispose)

```ts
dispose(): void
```

Stop listening for credential updates.

You rarely need it: a bundle built with [defineServer](https://gemarcade.com/developer/api/server#defineserver) is disposed for
you when the match ends. Call it in a test, or if you built the server
yourself and are tearing it down.

## ServerTokenGrant.token

[ServerTokenGrant.token](https://gemarcade.com/developer/api/server#servertokengrant-token)

```ts
readonly token: string
```

The credential to present. Never log it and never send it on.

## ServerTokenGrant.expiresAt

[ServerTokenGrant.expiresAt](https://gemarcade.com/developer/api/server#servertokengrant-expiresat)

```ts
readonly expiresAt: number
```

When it expires, in seconds since the Unix epoch.

## ServerBundleHost.matchId

[ServerBundleHost.matchId](https://gemarcade.com/developer/api/server#serverbundlehost-matchid)

```ts
readonly matchId: string
```

The match this server was allocated for.

## ServerBundleHost.gameId

[ServerBundleHost.gameId](https://gemarcade.com/developer/api/server#serverbundlehost-gameid)

```ts
readonly gameId: string
```

The game it belongs to.

## ServerBundleHost.channel

[ServerBundleHost.channel](https://gemarcade.com/developer/api/server#serverbundlehost-channel)

```ts
readonly channel: string
```

The channel it was allocated on — `dev`, `test` or `release`.

## ServerBundleHost.gameRoomId

[ServerBundleHost.gameRoomId](https://gemarcade.com/developer/api/server#serverbundlehost-gameroomid)

```ts
readonly gameRoomId?: string
```

The room this match came from. MAY BE ABSENT on older platform images, so
check before using it — that absence is also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can
be missing.

## ServerBundleHost.apiBaseUrl

[ServerBundleHost.apiBaseUrl](https://gemarcade.com/developer/api/server#serverbundlehost-apibaseurl)

```ts
readonly apiBaseUrl?: string
```

Where to send API calls. Absent on older platform images, which is the case
[createGemServer](https://gemarcade.com/developer/api/server#creategemserver) refuses at startup.

## ServerBundleHost.serverToken

[ServerBundleHost.serverToken](https://gemarcade.com/developer/api/server#serverbundlehost-servertoken)

```ts
serverToken(): ServerTokenGrant | undefined
```

The credential held right now, if the platform delivered one.

## ServerBundleHost.onServerToken

[ServerBundleHost.onServerToken](https://gemarcade.com/developer/api/server#serverbundlehost-onservertoken)

```ts
onServerToken(listener: (grant: ServerTokenGrant) => void): () => void
```

Be told when the credential is replaced.

Called straight away if one is already held, and again on every refresh.
Returns a function that stops the notifications.

You rarely call this. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) subscribes for you.

| Parameter | Type |
| --- | --- |
| `listener` | `(grant: ServerTokenGrant) => void` |

## SendOptions.reliable

[SendOptions.reliable](https://gemarcade.com/developer/api/server#sendoptions-reliable)

```ts
readonly reliable?: boolean
```

Ask for the message to arrive. Defaults to true.

`false` says you would rather it be dropped than delayed — position updates
that a later one supersedes. A transport is free to deliver it reliably
anyway; the receiver is still told what you asked for.

## SendOptions.ordered

[SendOptions.ordered](https://gemarcade.com/developer/api/server#sendoptions-ordered)

```ts
readonly ordered?: boolean
```

Ask for messages to arrive in the order you sent them. Defaults to true.

## BroadcastOptions.reliable

[BroadcastOptions.reliable](https://gemarcade.com/developer/api/server#broadcastoptions-reliable)

```ts
readonly reliable?: boolean
```

Ask for the message to arrive. Defaults to true.

`false` says you would rather it be dropped than delayed — position updates
that a later one supersedes. A transport is free to deliver it reliably
anyway; the receiver is still told what you asked for.

## BroadcastOptions.ordered

[BroadcastOptions.ordered](https://gemarcade.com/developer/api/server#broadcastoptions-ordered)

```ts
readonly ordered?: boolean
```

Ask for messages to arrive in the order you sent them. Defaults to true.

## BroadcastOptions.except

[BroadcastOptions.except](https://gemarcade.com/developer/api/server#broadcastoptions-except)

```ts
readonly except?: string
```

A player to leave out — usually the one whose action caused the broadcast.

## GemServerHostProfile.displayName

[GemServerHostProfile.displayName](https://gemarcade.com/developer/api/server#gemserverhostprofile-displayname)

```ts
readonly displayName?: string
```

The player's chosen name, as a plain string.

PREFER [GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile), which gives you the same name as
[PlayerText](https://gemarcade.com/developer/api/server#playertext) — a type that reminds you a stranger wrote it.

## GemServerHostProfile.displayTag

[GemServerHostProfile.displayTag](https://gemarcade.com/developer/api/server#gemserverhostprofile-displaytag)

```ts
readonly displayTag?: string
```

The platform-assigned tag beside the name.

## GemServerHostProfile.discriminator

[GemServerHostProfile.discriminator](https://gemarcade.com/developer/api/server#gemserverhostprofile-discriminator)

```ts
readonly discriminator?: string
```

The disambiguating suffix, when the player has one.

## GemServerHost.matchId

[GemServerHost.matchId](https://gemarcade.com/developer/api/server#gemserverhost-matchid)

```ts
readonly matchId: string
```

The match this server was allocated for.

## GemServerHost.gameId

[GemServerHost.gameId](https://gemarcade.com/developer/api/server#gemserverhost-gameid)

```ts
readonly gameId: string
```

The game it belongs to.

## GemServerHost.channel

[GemServerHost.channel](https://gemarcade.com/developer/api/server#gemserverhost-channel)

```ts
readonly channel: string
```

The channel it was allocated on — `dev`, `test` or `release`.

## GemServerHost.gameRoomId

[GemServerHost.gameRoomId](https://gemarcade.com/developer/api/server#gemserverhost-gameroomid)

```ts
readonly gameRoomId?: string
```

The room this match came from. MAY BE ABSENT on older platform images, so
check before using it — that absence is also why [GemServer.room](https://gemarcade.com/developer/api/server#gemserver-room) can
be missing.

## GemServerHost.apiBaseUrl

[GemServerHost.apiBaseUrl](https://gemarcade.com/developer/api/server#gemserverhost-apibaseurl)

```ts
readonly apiBaseUrl?: string
```

Where to send API calls. Absent on older platform images, which is the case
[createGemServer](https://gemarcade.com/developer/api/server#creategemserver) refuses at startup.

## GemServerHost.serverToken

[GemServerHost.serverToken](https://gemarcade.com/developer/api/server#gemserverhost-servertoken)

```ts
serverToken(): ServerTokenGrant | undefined
```

The credential held right now, if the platform delivered one.

## GemServerHost.onServerToken

[GemServerHost.onServerToken](https://gemarcade.com/developer/api/server#gemserverhost-onservertoken)

```ts
onServerToken(listener: (grant: ServerTokenGrant) => void): () => void
```

Be told when the credential is replaced.

Called straight away if one is already held, and again on every refresh.
Returns a function that stops the notifications.

You rarely call this. [createGemServer](https://gemarcade.com/developer/api/server#creategemserver) subscribes for you.

| Parameter | Type |
| --- | --- |
| `listener` | `(grant: ServerTokenGrant) => void` |

## GemServerHost.sizeClass

[GemServerHost.sizeClass](https://gemarcade.com/developer/api/server#gemserverhost-sizeclass)

```ts
readonly sizeClass: string
```

Which size of machine this server was allocated on.

## GemServerHost.roomArgs

[GemServerHost.roomArgs](https://gemarcade.com/developer/api/server#gemserverhost-roomargs)

```ts
readonly roomArgs?: Readonly<Record<string, RoomArgValue>>
```

**Deprecated.** Read `gem.room?.args()` instead — it gets the same settings from
the platform and distinguishes "no settings are declared" from "they are
declared and empty". Still delivered; not going away without notice.

The room's settings as they were at match start.

`undefined` covers two cases you cannot tell apart and do not need to: the
room type declares no settings, or the platform image is older than this
member.

## GemServerHost.roster

[GemServerHost.roster](https://gemarcade.com/developer/api/server#gemserverhost-roster)

```ts
readonly roster: readonly string[]
```

**Deprecated.** Read `gem.room?.get()` instead — live membership, with names,
roles and ready state. Still delivered; not going away without notice.

Who was in the room when this server was allocated.

A SIZING HINT, NOT A GUEST LIST. It is never revised, and it does not decide
who may connect — the platform admits players by credential. A player who
joined after allocation holds a valid one and is not in here.

## GemServerHost.players

[GemServerHost.players](https://gemarcade.com/developer/api/server#gemserverhost-players)

```ts
players(): string[]
```

The player ids connected right now, as a fresh array each call.

## GemServerHost.profile

[GemServerHost.profile](https://gemarcade.com/developer/api/server#gemserverhost-profile)

```ts
profile(playerId: string): GemServerHostProfile | undefined
```

A player's display identity, if the platform resolved one.

OPTIONAL TWICE OVER: the method itself may be absent on an older platform
image, and it may answer `undefined` for a player it has nothing for.
[GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile) collapses both into one `null`.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |

## GemServerHost.playerNumber

[GemServerHost.playerNumber](https://gemarcade.com/developer/api/server#gemserverhost-playernumber)

```ts
playerNumber(playerId: string): number | undefined
```

The 1-based seat the platform gave a connected player.

May be absent as a method on an older platform image, and answers
`undefined` for an id it has not seated.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |

## GemServerHost.send

[GemServerHost.send](https://gemarcade.com/developer/api/server#gemserverhost-send)

```ts
send(playerId: string, data: string | Uint8Array<ArrayBufferLike>, options?: SendOptions): void
```

Send to one player. A string goes as text; bytes go as binary, and the
receiver is told which.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` |  |
| `data` | `string \| Uint8Array<ArrayBufferLike>` |  |
| `options` | `SendOptions` | What delivery you are asking for — see [SendOptions](https://gemarcade.com/developer/api/server#sendoptions).
  Pass them freely: an older platform image ignores them harmlessly. |

## GemServerHost.broadcast

[GemServerHost.broadcast](https://gemarcade.com/developer/api/server#gemserverhost-broadcast)

```ts
broadcast(data: string | Uint8Array<ArrayBufferLike>, options?: BroadcastOptions): void
```

Send to every connected player. See [BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions) for how, and
for leaving one out.

| Parameter | Type |
| --- | --- |
| `data` | `string \| Uint8Array<ArrayBufferLike>` |
| `options` | `BroadcastOptions` |

## GemServerHost.kick

[GemServerHost.kick](https://gemarcade.com/developer/api/server#gemserverhost-kick)

```ts
kick(playerId: string, reason?: string): void
```

Disconnect a player. `reason` is shown to them and recorded in the logs.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |
| `reason` | `string` |

## GemServerHost.log

[GemServerHost.log](https://gemarcade.com/developer/api/server#gemserverhost-log)

```ts
log(message: string): void
```

Write a line to the match's logs.

THE ONLY ROUTE. `console.log` writes to standard output, which is how the
runtime talks to your server — a line written there arrives as a complaint
about your bundle rather than as a log.

| Parameter | Type |
| --- | --- |
| `message` | `string` |

## GemServerHost.warn

[GemServerHost.warn](https://gemarcade.com/developer/api/server#gemserverhost-warn)

```ts
warn(message: string): void
```

Like [GemServerHost.log](https://gemarcade.com/developer/api/server#gemserverhost-log), marked as a warning.

| Parameter | Type |
| --- | --- |
| `message` | `string` |

## HostTokenSourceOptions.clock

[HostTokenSourceOptions.clock](https://gemarcade.com/developer/api/server#hosttokensourceoptions-clock)

```ts
readonly clock: Clock
```

Where time comes from — used for expiry and for the wait below. Pass a
[Clock](https://gemarcade.com/developer/api/server#clock) in a test.

## HostTokenSourceOptions.timeoutMs

[HostTokenSourceOptions.timeoutMs](https://gemarcade.com/developer/api/server#hosttokensourceoptions-timeoutms)

```ts
readonly timeoutMs?: number
```

How long to wait for the platform to deliver a credential before giving up
on a call. Defaults to 10 seconds.

## HostTokenSourceOptions.onStateChange

[HostTokenSourceOptions.onStateChange](https://gemarcade.com/developer/api/server#hosttokensourceoptions-onstatechange)

```ts
readonly onStateChange?: (state: AuthState, reason?: AuthFailure) => void
```

Called when the ability to make authenticated calls changes. See
[AuthState](https://gemarcade.com/developer/api/server#authstate) for what to branch on.

## HostTokenSource.dispose

[HostTokenSource.dispose](https://gemarcade.com/developer/api/server#hosttokensource-dispose)

```ts
dispose(): void
```

Stop refreshing the credential.

CALL IT AT SHUTDOWN. A pending refresh timer keeps the process alive, so a
bundle that skips this looks like it has hung rather than exited.

## GemServerBundle.init

[GemServerBundle.init](https://gemarcade.com/developer/api/server#gemserverbundle-init)

```ts
init(): void | Promise<void>
```

Once, before any player arrives. Set your world up here.

YOU HAVE 60 SECONDS. Throwing fails the match, so do not start anything here
you are not prepared to have fail loudly.

## GemServerBundle.join

[GemServerBundle.join](https://gemarcade.com/developer/api/server#gemserverbundle-join)

```ts
join(playerId: string, meta?: { playerNumber?: number; transport?: string }): void | Promise<void>
```

A player connected, and the platform has already verified who they are.

They are ALREADY in `host.players()` by the time this runs.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Who joined. Take ids from here rather than from anything a
  client sent you. |
| `meta` | `{ playerNumber?: number; transport?: string }` | What the platform knows about the arrival — the seat it gave
  them, and which transport carried them. MAY BE ABSENT on older platform
  images, so default before use. |

## GemServerBundle.leave

[GemServerBundle.leave](https://gemarcade.com/developer/api/server#gemserverbundle-leave)

```ts
leave(playerId: string, meta?: { cause?: string }): void | Promise<void>
```

A player's connection closed. They are ALREADY gone from `host.players()`.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Who left. |
| `meta` | `{ cause?: string }` | Why, when the platform knows. May be absent. |

## GemServerBundle.message

[GemServerBundle.message](https://gemarcade.com/developer/api/server#gemserverbundle-message)

```ts
message(playerId: string, data: string | Uint8Array<ArrayBufferLike>, isText: boolean, meta?: { reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }): void | Promise<void>
```

A player sent you something.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Who sent it. |
| `data` | `string \| Uint8Array<ArrayBufferLike>` | The message: a string when `isText`, bytes otherwise. |
| `isText` | `boolean` | Which of the two `data` is. CHECK IT rather than guessing —
  treating bytes as text corrupts them. |
| `meta` | `{ reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }` | What the sender ASKED FOR — `reliable` and `ordered` — carried
  across whatever the transport actually did. May be absent. |

## GemServerBundle.shutdown

[GemServerBundle.shutdown](https://gemarcade.com/developer/api/server#gemserverbundle-shutdown)

```ts
shutdown(): void | Promise<void>
```

The match is ending, or the platform is reclaiming the machine. Save what
just happened.

YOU HAVE LONGER THAN YOU THINK, and how much depends on which of the two it
is. When the match ends: 10 minutes before the process goes — deliberately
generous, because writing results is what servers do here. When the platform
is reclaiming the machine: up to 2 hours, so a match with players still in it
plays out rather than dying mid-game.

**Guides.** [server-bundles](https://gemarcade.com/developer/docs/server-bundles#how-long-your-server-lives)

## GemServerLifecycle.init

[GemServerLifecycle.init](https://gemarcade.com/developer/api/server#gemserverlifecycle-init)

```ts
init(host: GemServerHost): Promise<void>
```

Called first, handing over the host object. This is where the SDK takes it
and where your own `init` runs.

| Parameter | Type |
| --- | --- |
| `host` | `GemServerHost` |

## GemServerLifecycle.join

[GemServerLifecycle.join](https://gemarcade.com/developer/api/server#gemserverlifecycle-join)

```ts
join(playerId: string, meta?: { playerNumber?: number; transport?: string }): void | Promise<void>
```

Forwarded from your bundle, when you defined a `join`.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |
| `meta` | `{ playerNumber?: number; transport?: string }` |

## GemServerLifecycle.leave

[GemServerLifecycle.leave](https://gemarcade.com/developer/api/server#gemserverlifecycle-leave)

```ts
leave(playerId: string, meta?: { cause?: string }): void | Promise<void>
```

Forwarded from your bundle, when you defined a `leave`.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |
| `meta` | `{ cause?: string }` |

## GemServerLifecycle.message

[GemServerLifecycle.message](https://gemarcade.com/developer/api/server#gemserverlifecycle-message)

```ts
message(playerId: string, data: string | Uint8Array<ArrayBufferLike>, isText: boolean, meta?: { reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }): void | Promise<void>
```

Forwarded from your bundle, when you defined a `message`.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |
| `data` | `string \| Uint8Array<ArrayBufferLike>` |
| `isText` | `boolean` |
| `meta` | `{ reliable?: boolean; ordered?: boolean; sessionId?: number; playerNumber?: number }` |

## GemServerLifecycle.shutdown

[GemServerLifecycle.shutdown](https://gemarcade.com/developer/api/server#gemserverlifecycle-shutdown)

```ts
shutdown(): Promise<void>
```

Always defined, even if your bundle has no `shutdown` — the SDK has teardown
of its own to do, and it runs whether or not yours threw.

## GemServerProfile.displayName

[GemServerProfile.displayName](https://gemarcade.com/developer/api/server#gemserverprofile-displayname)

```ts
readonly displayName: PlayerText
```

The name the player chose — so it is [PlayerText](https://gemarcade.com/developer/api/server#playertext), not a plain string.

The type is the reminder: this is text a stranger wrote. `text.toPlain` is
the one way to turn it into a string, and on a server the honest answer is
usually that you never need to.

## GemServerProfile.displayTag

[GemServerProfile.displayTag](https://gemarcade.com/developer/api/server#gemserverprofile-displaytag)

```ts
readonly displayTag: string
```

The platform-assigned tag beside the name. Not player-authored, so it is an
ordinary string.

## GemServerProfile.discriminator

[GemServerProfile.discriminator](https://gemarcade.com/developer/api/server#gemserverprofile-discriminator)

```ts
readonly discriminator?: string
```

The disambiguating suffix, when the player has one.

## GemMatchServer.identity

[GemMatchServer.identity](https://gemarcade.com/developer/api/server#gemmatchserver-identity)

```ts
readonly identity: GemServerIdentity
```

Which game, channel and match this server is running.

## GemMatchServer.client

[GemMatchServer.client](https://gemarcade.com/developer/api/server#gemmatchserver-client)

```ts
readonly client: GemClient
```

The HTTP client underneath [GemServer.api](https://gemarcade.com/developer/api/server#gemserver-api), for calls the generated
surface does not cover.

PREFER `api`. It is typed against the platform's description, so a shape
that changes becomes a build error in your bundle. This one takes a path and
checks nothing — reach for it when you genuinely need something `api` has
no method for, and treat that as worth telling us about.

## GemMatchServer.api

[GemMatchServer.api](https://gemarcade.com/developer/api/server#gemmatchserver-api)

```ts
readonly api: ServerApi
```

Every platform operation your server can call. This is the one you want.

See [ServerApi](https://gemarcade.com/developer/api/server#serverapi).

## GemMatchServer.match

[GemMatchServer.match](https://gemarcade.com/developer/api/server#gemmatchserver-match)

```ts
readonly match: { end: null }
```

Ending the match.

`end()` is HOW A SERVER-AUTHORITATIVE GAME FINISHES. It tells the platform
the match is over; the platform then tells the runtime, and your server
winds down through its ordinary shutdown. You do not stop the process
yourself.

It always means THIS match — the id is already known, so there is nothing to
pass and no way to end someone else's.

## GemMatchServer.match.end

[GemMatchServer.match.end](https://gemarcade.com/developer/api/server#gemmatchserver-match-end)

```ts
end(signal?: AbortSignal): Promise<void>
```

Tell the platform this match is finished.

| Parameter | Type | Description |
| --- | --- | --- |
| `signal` | `AbortSignal` | Cancels the call — not the match. |

## GemMatchServer.room

[GemMatchServer.room](https://gemarcade.com/developer/api/server#gemmatchserver-room)

```ts
readonly room?: { get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>; args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse> }
```

Reading the room this match came from — the two calls almost every server
makes at startup, already pointed at your own room.

CHECK IT IS THERE. `room` is absent on older platform images, which is why
the type says it might be. `gem.room?.get()` is the shape to write. When it
is absent the same reads are still available through `api` if you know the
room id from somewhere else.

## GemMatchServer.room.get

[GemMatchServer.room.get](https://gemarcade.com/developer/api/server#gemmatchserver-room-get)

```ts
get: (options?: { ifNoneMatch?: string; signal?: AbortSignal }) => Promise<VersionedResponse<GameRoomResponseV2>>
```

The room, in full: the settings it was created with, the template those
came from, and every member — with display name, role, ready state and
avatar.

This is what you want at `start()`, and it is a superset of what the older
roster push carried.

## GemMatchServer.room.args

[GemMatchServer.room.args](https://gemarcade.com/developer/api/server#gemmatchserver-room-args)

```ts
args: (options?: { signal?: AbortSignal }) => Promise<RoomArgsResponse>
```

Just the room's settings.

A room whose template declares no settings answers 404 rather than an
empty object — "this room has no settings" and "this room's settings are
empty" are different answers and the API keeps them apart.

## GemMatchServer.dispose

[GemMatchServer.dispose](https://gemarcade.com/developer/api/server#gemmatchserver-dispose)

```ts
dispose(): void
```

Stop listening for credential updates.

You rarely need it: a bundle built with [defineServer](https://gemarcade.com/developer/api/server#defineserver) is disposed for
you when the match ends. Call it in a test, or if you built the server
yourself and are tearing it down.

## GemMatchServer.sizeClass

[GemMatchServer.sizeClass](https://gemarcade.com/developer/api/server#gemmatchserver-sizeclass)

```ts
readonly sizeClass: string
```

Which size of machine this server was allocated on.

Useful if your game scales what it does to the hardware it got.

## GemMatchServer.roomArgs

[GemMatchServer.roomArgs](https://gemarcade.com/developer/api/server#gemmatchserver-roomargs)

```ts
readonly roomArgs: Readonly<Record<string, RoomArgValue>> | undefined
```

**Deprecated.** Use `gem.room?.args()` instead. It reads the same settings from
the platform and can tell "this room type has no settings" apart from "it has
settings and they are empty" — a difference this snapshot cannot express.
This is still delivered and is not going away without notice.

The room's settings as they were at match start.

## GemMatchServer.roster

[GemMatchServer.roster](https://gemarcade.com/developer/api/server#gemmatchserver-roster)

```ts
readonly roster: readonly string[]
```

**Deprecated.** Use `gem.room?.get()` instead: live membership, with names, roles
and ready state. This is still delivered and is not going away without notice.

Who was in the room when the server was allocated.

A HINT FOR SIZING, NOT A GUEST LIST. It is never revised, and it is not what
decides who may connect — the platform admits players by credential, not by
this array.

## GemMatchServer.players

[GemMatchServer.players](https://gemarcade.com/developer/api/server#gemmatchserver-players)

```ts
players(): string[]
```

The players connected right now, as a fresh array each call.

## GemMatchServer.profile

[GemMatchServer.profile](https://gemarcade.com/developer/api/server#gemmatchserver-profile)

```ts
profile(playerId: string): GemServerProfile | null
```

A connected player's display identity, or `null` if the platform has none
for them.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Take this from `join()` or `players()`, never from something
  a client sent you. |

## GemMatchServer.playerNumber

[GemMatchServer.playerNumber](https://gemarcade.com/developer/api/server#gemmatchserver-playernumber)

```ts
playerNumber(playerId: string): number | undefined
```

The seat number the platform gave a player — 1-based.

`undefined` when there is no answer: an id that is not seated, or an older
platform image that does not assign seats. You cannot tell those apart and
do not need to — handle the absence.

| Parameter | Type |
| --- | --- |
| `playerId` | `string` |

## GemMatchServer.send

[GemMatchServer.send](https://gemarcade.com/developer/api/server#gemmatchserver-send)

```ts
send(playerId: string, data: string | Uint8Array<ArrayBufferLike>, options?: SendOptions): void
```

Send to one player.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Who to send to. |
| `data` | `string \| Uint8Array<ArrayBufferLike>` | A string or bytes. The receiver is told which. |
| `options` | `SendOptions` | What delivery you are ASKING FOR — see [SendOptions](https://gemarcade.com/developer/api/server#sendoptions).
  Both default to true. Pass them freely: an older platform image ignores
  them harmlessly. |

## GemMatchServer.broadcast

[GemMatchServer.broadcast](https://gemarcade.com/developer/api/server#gemmatchserver-broadcast)

```ts
broadcast(data: string | Uint8Array<ArrayBufferLike>, options?: BroadcastOptions): void
```

Send to every connected player.

| Parameter | Type | Description |
| --- | --- | --- |
| `data` | `string \| Uint8Array<ArrayBufferLike>` | A string or bytes. |
| `options` | `BroadcastOptions` | See [BroadcastOptions](https://gemarcade.com/developer/api/server#broadcastoptions) — the same delivery choices as
  [GemMatchServer.send](https://gemarcade.com/developer/api/server#gemmatchserver-send), plus who to leave out. |

## GemMatchServer.kick

[GemMatchServer.kick](https://gemarcade.com/developer/api/server#gemmatchserver-kick)

```ts
kick(playerId: string, reason?: string): void
```

Disconnect a player.

| Parameter | Type | Description |
| --- | --- | --- |
| `playerId` | `string` | Who to remove. |
| `reason` | `string` | Shown to them, and recorded in the match's logs. |

## GemMatchServer.log

[GemMatchServer.log](https://gemarcade.com/developer/api/server#gemmatchserver-log)

```ts
log(message: string): void
```

Write a line to the match's logs.

USE THIS, NEVER `console.log`. Standard output belongs to the platform — it
is how the runtime talks to your server — and writing to it directly
corrupts that conversation.

| Parameter | Type |
| --- | --- |
| `message` | `string` |

## GemMatchServer.warn

[GemMatchServer.warn](https://gemarcade.com/developer/api/server#gemmatchserver-warn)

```ts
warn(message: string): void
```

Like [GemMatchServer.log](https://gemarcade.com/developer/api/server#gemmatchserver-log), marked as a warning.

| Parameter | Type |
| --- | --- |
| `message` | `string` |
