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, base_uri, home_uri and event_stream_callback_uri): the seed re-applies them at every start and keeps everything else. The display name is not in the configuration: it lives in the database, and the app owners change it here. Neither is the channel order: the seed ranks a channel once, when it creates it, and the owners then choose the main channel here.

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_channelchannel or nullThe main channel: the first channel in rank order. Same shape as a channel of GET /:app_id. null for an app with no channel.
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. An app with no channel at all, such as the eternaltwin app that owns the general forums, is visible to its owners and the administrators only.

  • 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).
oauth_allowed_scopesobject, absentWhat the OAuth client of the channel may request: {"base": true, "forum_read": true, "forum_write": true, "forum_moderate": false}, the implied scopes filled in. Absent when the channel has no OAuth client ({app}_{channel}@clients, or {app}@clients for production). Never in GET /.

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_namestringName in the main locale.
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.
main_channelstringKey of the channel to rank first, which makes it the main_channel of the app. The other channels keep their order. It must exist.
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.
allowed_scopesstringWhat the OAuth client of the channel may request, as an OAuth scope string: "base", "forum:read", "forum:write" or "forum:moderate"; the implied scopes are added. Narrowing it does not revoke the tokens already issued.

Example

PATCH /api/v1/apps/e1484871-29a7-41b1-8c3f-0aa334be1c40
{
  "kind": "Other",
  "display_name": "Alpha",
  "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"},
      "allowed_scopes": "forum:write"
    }
  ]
}

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",
        "oauth_allowed_scopes": {"base": true, "forum_read": true, "forum_write": true, "forum_moderate": false}
      }
    ]
  },
  "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 is_official. In owners, they may only add users: a list that leaves out a current owner is refused. In allowed_scopes, they choose between base, forum:read and forum:write: granting forum:moderate, or changing a channel that has it, is refused.
  • 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 removing owners, touching is_official or forum:moderate.
404{"error": "app not found"}Unknown app.
422{"error": "app channel not found"}, "unknown owner", "invalid `user_uri`", "invalid `allowed_scopes`" or "app channel without an OAuth client"Unknown channel key or main_channel, unknown owner, invalid user_uri, unknown scope, or allowed_scopes on a channel without an OAuth client.
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 app key.
description_i18ntableTranslations of the description, fr-FR = "…".
cover_imagestringName of a cover picture shipped with the website, box_….
[seed.app.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.

The configuration has no display name. A new app, and a new channel, are named after their key (neoparc, neoparc_staging); an owner then renames the app with display_name. A display_name key left in an older configuration is ignored. The OAuth consent screen shows the app's display name.