Eternaltwin

Home | /api | v1

/api/v1/apps

The registry of applications built on Eternaltwin: games, tools and the platform itself. Defined in crates/rest/src/apps.rs.

An app is the product; a channel is one deployment of it — main, beta, a local development instance — with its own URLs, rating and visibility. An app always has at least one channel.

There are three routes: two reads and one write. Apps are created by the seeding configuration ([seed.app] in eternaltwin.local.toml), not through this API. The write, PATCH /:app_id, edits an existing app but never touches the fields that come from the configuration (the app and channel keys, the main display_name, base_uri, home_uri and event_stream_callback_uri): the seed re-applies them at every start and keeps everything else.

This is not app, which is the desktop client's release feed.

GET /

Lists the registered applications, as a listing of short apps. Authenticated or not. owners and i18n are filled.

The list is filtered by visibility (see below): an app is returned only if the caller can see at least one of its channels. count is the number of apps returned on the page, after this filtering.

Neither limit nor offset is read from the query today: the handler passes None for both and the service applies its own defaults, a limit of 100.

Example

GET /api/v1/apps
{
  "offset": 0,
  "limit": 100,
  "count": 2,
  "items": [
    {
      "id": "e1484871-29a7-41b1-8c3f-0aa334be1c40",
      "created_at": "2021-01-15T14:17:14.015Z",
      "deleted_at": null,
      "key": "alpha_dev",
      "owners": [
        {"type": "User", "id": "9f310484-963b-446b-af69-797feec6813f"}
      ],
      "channels": {
        "offset": 0,
        "limit": 10,
        "count": 1,
        "items": [{"key": "main"}]
      },
      "main_channel": {
        "id": "5d2b1f18-0c3a-4f9b-8f0b-4f7f8a9c1d20",
        "key": "main",
        "period": {"start": "2021-01-15T14:17:14.015Z", "end": null},
        "main_locale": "en-US",
        "description": "Raise your Dinoz",
        "description_i18n": {"fr-FR": "Élevez vos Dinoz"},
        "base_uri": "http://alpha.localhost/",
        "home_uri": "http://alpha.localhost/",
        "user_uri": null,
        "rating": "General",
        "status": "Stable",
        "visibility": "Public",
        "is_official": false,
        "event_stream_callback_uri": null,
        "cover_image": {"type": "Asset", "name": "box_alpha"},
        "cover_color": null
      },
      "kind": "Game",
      "display_name": "alpha",
      "main_locale": "en-US",
      "i18n": {"fr-FR": {"display_name": "alpha"}}
    }
  ]
}
FieldTypeMeaning
idUUIDIdentifier of the app.
created_attimestampWhen it was registered.
deleted_attimestamp or nullWhen it was retired.
keystring or nullStable handle, ^[a-z][a-z_0-9]{0,99}$.
ownersarrayUser references, {"type": "User", "id": …}.
channelslistingOne entry per visible channel, each {"key": …} only. count counts the visible channels.
main_channelchannelThe main channel: the first channel in rank order. Same shape as a channel of GET /:app_id.
kindstring"Platform", "Game" or "Other".
display_namestringName shown to members.
main_localestringLocale the display_name is written in.
i18nobjectPer-locale overrides, keyed by locale.

Errors

StatusBodyWhen
400{"error": "invalid \limit` query parameter"}`The store refused the limit.
500{"error": "internal error"}—

Visibility

A channel is visible to the caller if its visibility is "Public", or if the caller is an administrator, or if the caller is an owner of the app. An app is visible if its main channel, the first one in rank order, is visible: an app whose main channel is "Private" is hidden from everybody but its owners and the administrators, even if another of its channels is public.

  • GET / omits the apps that are not visible.
  • GET /:app_id answers 404 for an app that is not visible, and omits the channels that are not visible from the others.

GET /:app_id

Returns one application, with its visible channels expanded. :app_id is the UUID — this route does not resolve a key.

Example

GET /api/v1/apps/e1484871-29a7-41b1-8c3f-0aa334be1c40
{
  "id": "e1484871-29a7-41b1-8c3f-0aa334be1c40",
  "period": {
    "start": "2021-01-15T14:17:14.015Z",
    "end": null
  },
  "key": "alpha_dev",
  "owners": [
    {"type": "User", "id": "9f310484-963b-446b-af69-797feec6813f"}
  ],
  "channels": {
    "offset": 0,
    "limit": 10,
    "count": 1,
    "items": [
      {
        "id": "5d2b1f18-0c3a-4f9b-8f0b-4f7f8a9c1d20",
        "key": "main",
        "period": {"start": "2021-01-15T14:17:14.015Z", "end": null},
        "main_locale": "en-US",
        "description": "",
        "description_i18n": {},
        "base_uri": "http://alpha.localhost/",
        "home_uri": "http://alpha.localhost/",
        "user_uri": null,
        "rating": "General",
        "status": "Stable",
        "visibility": "Public",
        "is_official": false,
        "event_stream_callback_uri": null,
        "cover_image": null,
        "cover_color": null
      }
    ]
  },
  "kind": "Game",
  "display_name": "alpha",
  "main_locale": "en-US",
  "i18n": {}
}

The full app carries period where the short one carries created_at and deleted_at; it is the same fact in the shape the rest of the codebase uses for a lifetime, {"start": …, "end": null} while the app is live.

Channel fields worth naming:

FieldTypeMeaning
descriptionstringDescription, written in the channel main_locale.
description_i18nobjectTranslations of the description, {locale: text}.
base_uriURLRoot of the deployment.
home_uriURLWhere a member is sent when they click the app.
user_uriURL template or nullProfile page on the app. Contains a {user_id} path component.
ratingstring"General", "Teen" or "Adult".
statusstring"Stable", "Beta", "Alpha" or "Dev".
visibilitystring"Public" or "Private".
is_officialbooleanEndorsed by the Eternaltwin team.
cover_imageobject or nullPicture on the front of the box: {"type": "Asset", "name": "box_…"}, where name matches ^box_[a-z0-9_]+$ and designates a picture shipped with the website. null when there is none.
cover_colorstring or null#rrggbb hexadecimal, lowercase (despite the ColorHex8 type name, the pattern accepts 6 digits).

Status

StatusWhen
200The app exists and is live.
404{"error": "app not found"}. Also returned when the app exists but the caller sees none of its channels.
410The app is retired. The body is still the app object, with a closed period.
500{"error": "internal error"}.

PATCH /:app_id

Edits an application and, optionally, some of its channels. Returns the updated app, in the same shape as GET /:app_id. Only available when the server runs on Postgres: with the in-memory store the route answers 500{"error": "internal error"} (the store reports not implemented).

Every field is optional. An absent field is left unchanged. null clears user_uri, cover_color and cover_image; it is not valid for the other fields.

Body

FieldTypeMeaning
kindstring"Platform", "Game" or "Other".
main_localestringLocale the main display_name is written in.
display_name_i18nobject{locale: name}. Replaces all the translations. An entry for the main locale is ignored.
ownersarrayReplaces the owners: [{"type": "User", "id": …}], UUIDs only.
channelsarrayChannel patches, see below. Channels not listed are unchanged.

Each entry of channels:

FieldTypeMeaning
keystringRequired. Channel to edit. It must exist.
main_localestringLocale of the description.
descriptionstringDescription of the channel.
description_i18nobject{locale: text}. Replaces all the translations of the description. An entry for the channel main_locale is ignored.
user_uriURL template or nullProfile page on the app, with a {user_id} component. null clears it.
ratingstring"General", "Teen" or "Adult".
statusstring"Stable", "Beta", "Alpha" or "Dev".
visibilitystring"Public" or "Private".
is_officialbooleanEndorsed by the Eternaltwin team. Administrators only.
cover_imageobject or null{"type": "Asset", "name": "box_…"}, name matching ^box_[a-z0-9_]+$. null clears it. Any other value is rejected.
cover_colorstring or null#rrggbb hexadecimal, lowercase (despite the ColorHex8 type name, the pattern accepts 6 digits). null clears it.

Example

PATCH /api/v1/apps/e1484871-29a7-41b1-8c3f-0aa334be1c40
{
  "kind": "Other",
  "display_name_i18n": {"fr-FR": "alpha"},
  "channels": [
    {
      "key": "main",
      "rating": "Teen",
      "cover_color": "#ff8800",
      "user_uri": null,
      "cover_image": {"type": "Asset", "name": "box_alpha"},
      "description_i18n": {"fr-FR": "Un jeu"}
    }
  ]
}

The response is 200 with the updated app:

{
  "id": "e1484871-29a7-41b1-8c3f-0aa334be1c40",
  "period": {"start": "2021-01-15T14:17:14.015Z", "end": null},
  "key": "alpha_dev",
  "owners": [
    {"type": "User", "id": "9f310484-963b-446b-af69-797feec6813f"}
  ],
  "channels": {
    "offset": 0,
    "limit": 10,
    "count": 1,
    "items": [
      {
        "id": "5d2b1f18-0c3a-4f9b-8f0b-4f7f8a9c1d20",
        "key": "main",
        "period": {"start": "2021-01-15T14:17:14.015Z", "end": null},
        "main_locale": "en-US",
        "description": "",
        "description_i18n": {"fr-FR": "Un jeu"},
        "base_uri": "http://alpha.localhost/",
        "home_uri": "http://alpha.localhost/",
        "user_uri": null,
        "rating": "Teen",
        "status": "Stable",
        "visibility": "Public",
        "is_official": false,
        "event_stream_callback_uri": null,
        "cover_image": {"type": "Asset", "name": "box_alpha"},
        "cover_color": "#ff8800"
      }
    ]
  },
  "kind": "Other",
  "display_name": "alpha",
  "main_locale": "en-US",
  "i18n": {"fr-FR": {"display_name": "alpha"}}
}

Rights

  • An administrator can change everything.
  • An owner of the app can change everything except owners and is_official.
  • Any other user is refused with 403; a guest with 401.

Status

StatusBodyWhen
200The updated app.The patch was applied.
401{"error": "authentication required"}Guest.
403{"error": "forbidden"}Not an administrator or an owner, or an owner touching owners or is_official.
404{"error": "app not found"}Unknown app.
422{"error": "app channel not found"}, "unknown owner" or "invalid `user_uri`"Unknown channel key, unknown owner, or invalid user_uri.
500{"error": "internal error"}Also returned without Postgres.

A body that is not valid JSON, or that has a field of the wrong type, gets the default axum rejection (400 or 422).

Seeding

The apps are created by the seeding configuration, [seed.app.<key>]. Besides the fields that define the app itself, three fields fill the main channel's presentation:

FieldTypeMeaning
descriptionstringDescription in the main locale (en-US). Defaults to the display name.
description_i18ntableTranslations of the description, fr-FR = "…".
cover_imagestringName of a cover picture shipped with the website, box_….
[seed.app.neoparc]
display_name = "Neoparc"
description = "Raise your Dinoz"
cover_image = "box_neoparc"

[seed.app.neoparc.description_i18n]
fr-FR = "Élevez vos Dinoz"

These three fields are applied when the app is created, and at every later start only while the stored value is still the default an older seed left: a description equal to the channel's name, no translation, no cover. Once a value has been set, here or through PATCH /:app_id, a later seed never overwrites it. This lets the configuration fill apps that existed before these fields did.