# A backend with server keys

Server keys and their scopes, players by your own user id, a token handed to the browser, leaderboards and statistics from your servers.

A server key lets your backend act for your site on `/v1/server/*`: create a Filudi player for each of your
users, give the browser a token for that player, read leaderboards with nicknames, read the site's numbers.
The key never reaches a browser and nothing else of the API takes it.

## Keys and scopes

Create a key in the console, under the site, Keys: a name and the scopes it needs. The answer shows the key
once, `fsk_<site slug>_<32 base62 characters>`; the API keeps a hash and the console shows the first 12
characters from then on. Store it in your backend's environment. Only an approved site has keys, 20 at most,
revoked ones included.

| Scope | Routes |
| --- | --- |
| `catalog` | `GET /v1/server/catalog` |
| `players` | `POST /v1/server/players`, `GET /v1/server/players/{playerId}` |
| `tokens` | `POST /v1/server/players/{playerId}/tokens`, `POST /v1/server/game-tokens` |
| `leaderboards` | `GET /v1/server/leaderboards/{gameId}/{board}` |
| `stats` | `GET /v1/server/stats/overview`, `GET /v1/server/stats/games/{gameId}` |

A key without the scope of a route answers 403, as does a revoked key or a key of a site that lost its
approval; `message` says which. 600 requests a minute per key, then 429 with `Retry-After`. No IP limit, no CORS.
Revoke a key from the console: the row stays with `revokedAt` and every call with it answers 403 from then on.

<CodePanel title="Create a key">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{ "name": "string", "scopes": ["players", "tokens", "leaderboards"] }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // From the console, in the browser. The answer carries `key` once.
    const response = await fetch(`https://api.filudi.com/v1/admin/sites/${siteId}/keys`, {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ name: 'string', scopes: ['players', 'tokens', 'leaderboards'] }),
    });
    const { key, prefix } = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    # The console does this for you. Keep the key in your backend's environment:
    #   FILUDI_SITE_KEY=fsk_...
    import os

    site_key = os.environ['FILUDI_SITE_KEY']
    ```

  </Fragment>
</CodePanel>

## Players by your own user id

`POST /v1/server/players { externalId, nickname?, locale? }` creates a player for one of your users, or finds
the one created before: `externalId` is unique per site, so the call is safe to repeat. A new player answers
201, an existing one 200. A player created here is a guest with no token until you ask for one. `nickname` and
`locale` apply on creation only; the player changes them later with their own token on
`PATCH /v1/sdk/players/me`.

`GET /v1/server/players/{playerId}` reads the profile, the saves metadata and the best scores. A player of
another site answers 404.

<CodePanel title="Find or create a player">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/server/players' \
      -H "authorization: Bearer $FILUDI_SITE_KEY" \
      -H 'content-type: application/json' \
      -d '{ "externalId": "string", "nickname": "Pebble", "locale": "en" }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // Node, in your backend, on each sign-in of one of your users.
    async function filudiPlayer(user) {
      const response = await fetch('https://api.filudi.com/v1/server/players', {
        method: 'POST',
        headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}`, 'content-type': 'application/json' },
        body: JSON.stringify({ externalId: user.id, nickname: user.displayName, locale: user.locale }),
      });
      if (!response.ok) throw new Error(`filudi: ${response.status}`);
      return response.json(); // { id, externalId, nickname, locale, hasAccount, createdAt, lastSeenAt }
    }
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import os
    import requests

    def filudi_player(user):
        response = requests.post(
            'https://api.filudi.com/v1/server/players',
            headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"},
            json={'externalId': user.id, 'nickname': user.display_name, 'locale': user.locale},
        )
        response.raise_for_status()
        return response.json()
    ```

  </Fragment>
</CodePanel>

## Handing a token to the browser

The page needs a player token to run a game as filudi.com does. `POST /v1/server/players/{playerId}/tokens`
answers an `fp1` token, the same kind `POST /v1/sdk/players` answers in the browser. Give it to the page over
your own authenticated channel: the answer of your sign-in, a page you render for a signed-in user, a short
`fetch` on your origin. Never in a URL, never in a cookie readable by scripts of other origins.

In the page, the token goes where a guest token would: `localStorage` under `filudi:player:<site>`, then
`PlayerClient` or your own code uses it. The page exchanges it for a game token before each frame, as any
front does. A backend that renders the whole page can skip that step too: `POST /v1/server/game-tokens
{ playerId, game }` answers the `fg1` token for the frame, and the page posts it in `filudi:init` without
holding the player token at all.

<CodePanel title="A token for the browser">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // Your route, called by your page after sign-in. The token goes in the response body, not in a URL.
    app.post('/api/filudi-token', requireSession, async (request, reply) => {
      const player = await filudiPlayer(request.user);
      const response = await fetch(`https://api.filudi.com/v1/server/players/${player.id}/tokens`, {
        method: 'POST',
        headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}` },
      });
      const { token } = await response.json();
      reply.header('cache-control', 'private, no-store').send({ token });
    });
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    @app.post('/api/filudi-token')
    def filudi_token():
        player = filudi_player(current_user())
        response = requests.post(
            f"https://api.filudi.com/v1/server/players/{player['id']}/tokens",
            headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"},
        )
        response.raise_for_status()
        return {'token': response.json()['token']}, 200, {'cache-control': 'private, no-store'}
    ```

  </Fragment>
</CodePanel>

The whole exchange, from your user's sign-in to the game calling the API:

<figure class="not-content">
  <svg viewBox="0 0 560 420" width="100%" role="img" aria-labelledby="seq-title seq-desc" font-size="12">
    <title id="seq-title">Sequence of a backend with a server key</title>
    <desc id="seq-desc">The browser signs in to your backend. Your backend creates or finds the Filudi player by externalId, asks for a player token, and sends it to the browser. The browser exchanges it for a game token and posts filudi:init to the game frame, which then calls the Filudi API with that game token.</desc>
    <defs>
      <marker id="seq-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
        <path d="M 0 0 L 10 5 L 0 10 z" fill="var(--text-2)" />
      </marker>
    </defs>
    <g fill="var(--surface)" stroke="var(--line)" stroke-width="1.5">
      <rect x="10" y="10" width="110" height="34" rx="10" />
      <rect x="150" y="10" width="110" height="34" rx="10" />
      <rect x="290" y="10" width="110" height="34" rx="10" />
      <rect x="430" y="10" width="110" height="34" rx="10" />
    </g>
    <g fill="var(--text)" font-weight="700" text-anchor="middle">
      <text x="65" y="32">Browser</text>
      <text x="205" y="32">Your backend</text>
      <text x="345" y="32">Filudi API</text>
      <text x="485" y="32">Game frame</text>
    </g>
    <g stroke="var(--line)" stroke-width="1.5" stroke-dasharray="4 4">
      <line x1="65" y1="44" x2="65" y2="410" />
      <line x1="205" y1="44" x2="205" y2="410" />
      <line x1="345" y1="44" x2="345" y2="410" />
      <line x1="485" y1="44" x2="485" y2="410" />
    </g>
    <g stroke="var(--text-2)" stroke-width="1.5" marker-end="url(#seq-arrow)">
      <line x1="65" y1="80" x2="203" y2="80" />
      <line x1="205" y1="125" x2="343" y2="125" />
      <line x1="345" y1="160" x2="207" y2="160" stroke-dasharray="5 3" />
      <line x1="205" y1="205" x2="343" y2="205" />
      <line x1="345" y1="240" x2="207" y2="240" stroke-dasharray="5 3" />
      <line x1="205" y1="285" x2="67" y2="285" />
      <line x1="65" y1="330" x2="343" y2="330" />
      <line x1="345" y1="352" x2="67" y2="352" stroke-dasharray="5 3" />
      <line x1="65" y1="385" x2="483" y2="385" />
      <line x1="485" y1="405" x2="347" y2="405" />
    </g>
    <g fill="var(--text)" text-anchor="middle">
      <text x="134" y="72">signs in to your site</text>
      <text x="275" y="117">POST /v1/server/players</text>
      <text x="275" y="152" fill="var(--text-2)">player id</text>
      <text x="275" y="197">POST /v1/server/players/[id]/tokens</text>
      <text x="275" y="232" fill="var(--text-2)">fp1 player token</text>
      <text x="134" y="277">token in your own response</text>
      <text x="204" y="322">POST /v1/sdk/game-tokens</text>
      <text x="204" y="344" fill="var(--text-2)">fg1 game token</text>
      <text x="275" y="377">filudi:init with the fg1 token</text>
      <text x="415" y="399" fill="var(--text-2)">saves, scores, events</text>
    </g>
  </svg>
</figure>

Dashed arrows are answers. Solid ones are calls. The server key travels only on the arrows that leave your
backend; the player token never leaves the browser; the frame sees the game token alone.

## Leaderboards with nicknames

`GET /v1/server/leaderboards/{gameId}/{board}?period=week&limit=10` answers the same shape as the browser
route, with the nickname of each player. `isMe` is always false: a server has no "me". Use it for a page
rendered on the server, a Discord bot, a weekly email. The browser route stays the one to use inside a page,
where the player's own entry matters.

<CodePanel title="Weekly top">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/server/leaderboards/gloop/height?period=week&limit=10' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch(
      'https://api.filudi.com/v1/server/leaderboards/gloop/height?period=week&limit=10',
      { headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}` } },
    );
    const { entries, total } = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    response = requests.get(
        'https://api.filudi.com/v1/server/leaderboards/gloop/height',
        params={'period': 'week', 'limit': 10},
        headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"},
    )
    entries = response.json()['entries']
    ```

  </Fragment>
</CodePanel>

## Statistics

`GET /v1/server/stats/overview?from&to` and `GET /v1/server/stats/games/{gameId}?from&to` answer what the
console shows for the site: the same shapes as the admin routes, cached 60 seconds, 400 days at most.
[Statistics](/docs/portals/statistics/) lists the fields.

## What a backend cannot do

- Store a player token. It has no expiry and opens the player's account; it belongs in the player's browser.
  Ask for a new one on each sign-in instead.
- Act on a player's saves or scores. Saves and scores are written by the game, in the browser, with a game
  token; the API signs scores per play session and a server cannot forge that signature.
- Call `/v1/admin/*`. The console API takes a session cookie, not a key. Automation of the console is not
  offered in this release.
- Reach players of another site, or of filudi.com. A player id from your site means nothing elsewhere.
