/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"}}
}
]
}
| Field | Type | Meaning |
|---|---|---|
id | UUID | Identifier of the app. |
created_at | timestamp | When it was registered. |
deleted_at | timestamp or null | When it was retired. |
key | string or null | Stable handle, ^[a-z][a-z_0-9]{0,99}$. |
owners | array | User references, {"type": "User", "id": …}. |
channels | listing | One entry per visible channel, each {"key": …} only. count counts the visible channels. |
main_channel | channel | The main channel: the first channel in rank order. Same shape as a channel of GET /:app_id. |
kind | string | "Platform", "Game" or "Other". |
display_name | string | Name shown to members. |
main_locale | string | Locale the display_name is written in. |
i18n | object | Per-locale overrides, keyed by locale. |
Errors
| Status | Body | When |
|---|---|---|
| 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_idanswers404for 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:
| Field | Type | Meaning |
|---|---|---|
description | string | Description, written in the channel main_locale. |
description_i18n | object | Translations of the description, {locale: text}. |
base_uri | URL | Root of the deployment. |
home_uri | URL | Where a member is sent when they click the app. |
user_uri | URL template or null | Profile page on the app. Contains a {user_id} path component. |
rating | string | "General", "Teen" or "Adult". |
status | string | "Stable", "Beta", "Alpha" or "Dev". |
visibility | string | "Public" or "Private". |
is_official | boolean | Endorsed by the Eternaltwin team. |
cover_image | object or null | Picture 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_color | string or null | #rrggbb hexadecimal, lowercase (despite the ColorHex8 type name, the pattern accepts 6 digits). |
Status
| Status | When |
|---|---|
| 200 | The app exists and is live. |
| 404 | {"error": "app not found"}. Also returned when the app exists but the caller sees none of its channels. |
| 410 | The 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
| Field | Type | Meaning |
|---|---|---|
kind | string | "Platform", "Game" or "Other". |
main_locale | string | Locale the main display_name is written in. |
display_name_i18n | object | {locale: name}. Replaces all the translations. An entry for the main locale is ignored. |
owners | array | Replaces the owners: [{"type": "User", "id": …}], UUIDs only. |
channels | array | Channel patches, see below. Channels not listed are unchanged. |
Each entry of channels:
| Field | Type | Meaning |
|---|---|---|
key | string | Required. Channel to edit. It must exist. |
main_locale | string | Locale of the description. |
description | string | Description of the channel. |
description_i18n | object | {locale: text}. Replaces all the translations of the description. An entry for the channel main_locale is ignored. |
user_uri | URL template or null | Profile page on the app, with a {user_id} component. null clears it. |
rating | string | "General", "Teen" or "Adult". |
status | string | "Stable", "Beta", "Alpha" or "Dev". |
visibility | string | "Public" or "Private". |
is_official | boolean | Endorsed by the Eternaltwin team. Administrators only. |
cover_image | object or null | {"type": "Asset", "name": "box_…"}, name matching ^box_[a-z0-9_]+$. null clears it. Any other value is rejected. |
cover_color | string 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
ownersandis_official. - Any other user is refused with
403; a guest with401.
Status
| Status | Body | When |
|---|---|---|
| 200 | The 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:
| Field | Type | Meaning |
|---|---|---|
description | string | Description in the main locale (en-US). Defaults to the display name. |
description_i18n | table | Translations of the description, fr-FR = "…". |
cover_image | string | Name 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.