Loreset Documentation

Loreset is a Headless CMS for hierarchical and polymorphic game content. This page collects the documentation for the current version of the platform: the LSL schema language specification, the export formats (bundle-v1 and versions.json), and descriptions of the implemented subsystems.

Platform overview

The platform follows an API-first design: the only interface is REST API v1 with JWT authentication. All responses use a single contract:

{ "success": true, "data": { … }, "message": null, "errors": null }

Component overview:

Frontend SPA ──JWT Bearer──> REST API (v1)
                               │
                ┌──────────────┼──────────────────┐
                │              │                  │
           AclManager     LslValidator       ExportBuilder
                │              │                  │
                └──────> ItemStorage         Publisher (S3/local)
                               │                  │
                          I18nIndexer          CDN (S3)
                               │
                         i18n_registry

Data follows the Hybrid Storage principle: metadata, relations and versions live in relational tables (SQLite or MySQL), while the objects themselves, with arbitrary nesting, are stored as JSON documents. JSON columns are stored as TEXT for portability across databases.

Key concepts

ConceptDescription
ProjectA game. Contains collections, members, assets and CDN settings.
CollectionA named set of records described by an LSL schema. Its code is unique within the project. The is_singleton flag limits the collection to a single record (e.g. global balance settings).
SchemaAn immutable version of a collection's LSL definition. Publishing a new version moves the HEAD pointer current_schema_id; old versions remain available.
ItemHolds no data — only a status (draftpublishedarchived) and a HEAD pointer to the current revision.
RevisionAn immutable JSON snapshot of an item's data. Every edit adds a revision with version + 1. History is never rewritten.
PublicationAn upload of a versioned content bundle and the versions.json index to the project's CDN.

Implemented features

The current version of the platform includes the following subsystems:

Planned: a Godot plugin, a Unity package, data migrations on schema changes, background i18n indexing, translation export to .po / .xliff, refresh tokens.

Loreset Schema Language (LSL)

LSL is the language for describing collection schemas. It's based on JSON Schema but extended with game-specific data types and UI hints that the admin UI uses to generate editing forms automatically. A schema is a plain JSON file that's easy to keep in Git (Schema-as-Code).

A minimal collection schema:

{
  "type": "object",
  "properties": {
    "title": { "type": "i18n_string", "ui": { "label": "Title" } },
    "trigger_event": {
      "type": "enum",
      "options": ["first_login", "loot_drop", "level_complete"]
    },
    "is_hidden": { "type": "boolean" }
  },
  "required": ["title", "trigger_event"]
}

Reusable blocks go into $defs and are referenced via {"$ref": "#/$defs/Name"}. Every field may carry a ui object with a caption (label) and a display component (component) — e.g. searchable_select, sortable_list, tabs.

LSL data types

TypeValue in data
integer, float, booleanthe corresponding scalars
string, text, richtexta string (text is multiline, richtext supports <ref/> tags linking to other records)
timestampunix time (int) or a date string
durationnumber of seconds
assetan asset id (string)
i18n_string, i18n_textan object {"_i18n": true, "en": "...", "ru": "..."}
enumone of options (scalars or {value, label} objects)
relationthe id of a record in the target collection (target); with value_field: "id" the target's existence is verified
arraya list; every element is validated against items
dictionarya "key → value" object; values are validated against the values schema
unionan object with a discriminator field; the variant is picked from variants
objecta nested object with properties / required

relation — links between collections

"hero": {
  "type": "relation",
  "target": "heroes",
  "value_field": "id",
  "display_template": "{{id}}. {{name}} ({{class}})",
  "filters": { "is_playable": { "eq": true } },
  "ui": { "label": "Hero", "component": "searchable_select" }
}

union — polymorphic structures

The key type for games: a field accepts one of several structures depending on the discriminator value.

// Schema
{
  "type": "union",
  "discriminator": "reward_type",
  "variants": {
    "currency": { "$ref": "#/$defs/RewardCurrency" },
    "item":     { "$ref": "#/$defs/RewardItem" }
  }
}

// Valid data
{ "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }

dictionary — "key → value" maps

"currency_wallet": {
  "type": "dictionary",
  "keys": { "type": "relation", "target": "currencies" },
  "values": { "type": "integer" },
  "ui": { "label": "Wallet (Currency → Amount)" }
}

Data validation rules

Item data is validated against the collection's current schema before every save; invalid data is never persisted.

{
  "success": false,
  "message": "Item data does not match the collection schema.",
  "errors": {
    "$.trigger_event": "Value is not in the list of allowed options.",
    "$.rewards[0].amount": "Expected an integer."
  }
}

Content bundle (loreset/bundle-v1)

A single self-contained JSON for game engines: schemas, published records and a flat translation dictionary. Used both for exporting to the client and for moving content between environments (Dev → Prod).

{
  "format": "loreset/bundle-v1",
  "project": "heliostorm",
  "version": 7,
  "generated_at": "2026-07-08T12:00:00+00:00",
  "checksum": "sha256:ab12…",
  "schemas": {
    "bonuses": { "version": 2, "schema": { … } }
  },
  "items": {
    "bonuses": [
      {
        "id": "93a01703-…",
        "data": {
          "title": { "en": "Welcome Pack", "ru": "Приветственный набор" },
          "trigger_event": "first_login"
        }
      }
    ]
  },
  "i18n": {
    "bonuses.93a01703-….title": { "en": "Welcome Pack", "ru": "Приветственный набор" }
  }
}

Export — GET /v1/projects/{id}/export. Import — POST /v1/projects/{id}/import with the bundle as the body: missing collections are created, changed schemas are published as new versions, records are inserted or updated by id, and re-importing the same bundle is a no-op. Everything runs in a single transaction.

CDN version index (versions.json)

On every successful publication, a versions.json file — an index of all successful publications of the project — is rewritten next to the bundle-v{N}.json bundle. With a single static file request the game client learns the current content version and the bundle URL without calling the API:

<base_url>/<prefix>/versions.json
// e.g.: https://cdn.example.com/content/versions.json

File format

{
  "format": "loreset/versions-v1",
  "project": "my-rpg",
  "generated_at": 1783508000,
  "latest": 7,
  "versions": [
    {
      "version": 7,
      "file": "bundle-v7.json",
      "url": "https://cdn.example.com/content/bundle-v7.json",
      "checksum": "sha256:ab12…",
      "items_count": 132,
      "published_at": 1783508000
    },
    {
      "version": 6,
      "file": "bundle-v6.json",
      "url": "https://cdn.example.com/content/bundle-v6.json",
      "checksum": "sha256:cd34…",
      "items_count": 130,
      "published_at": 1783420000
    }
  ]
}

Top-level fields

FieldTypeDescription
formatstringFormat marker, always loreset/versions-v1
projectstringProject code
generated_atintUnix time when the index was generated
latestintNumber of the latest (current) bundle version
versionsarrayList of successful publications, newest first

versions array element

FieldTypeDescription
versionintBundle version number (grows monotonically within the project)
filestringBundle file name; the public link is assembled as <base_url>/<prefix>/<file>
urlstringThe file URL captured at publication time
checksumstringChecksum (sha256:…), matches the checksum field inside the bundle
items_countintNumber of records in the bundle
published_atintUnix time of the publication
Note: publications with the failed status never appear in the index. If uploading versions.json fails, the whole publication is marked failed.

Recommended client update flow

  1. Download versions.json.
  2. Compare latest with the version of the local bundle copy.
  3. If newer — download the file from the entry with version == latest and verify the checksum.
  4. Treat any network error gracefully: keep running on the current version.

Items and revisions

An item stores only its status and a HEAD pointer; the data lives in immutable revisions. Any change — an edit, a translation, a rollback, an import — creates a new revision and moves HEAD.

# Create an item (validated against the current schema)
curl -X POST https://api.loreset.dev/v1/collections/<uuid>/items \
  -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{
    "status": "draft",
    "data": {
      "title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" },
      "trigger_event": "first_login",
      "rewards": [
        { "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }
      ]
    }
  }'

Rollback never deletes history: the old revision's data is copied into a new revision and HEAD moves to it. With v1…v3 in history, a rollback creates v4 carrying the chosen revision's data:

curl -X POST https://api.loreset.dev/v1/items/<uuid>/rollback \
  -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{"revision_id": "<uuid-of-the-old-revision>"}'

Item statuses: draftpublishedarchived (transitions in any direction). Only published records make it into the export bundle. A singleton collection can hold only one record.

Localization (i18n)

Translations live inside the item's data — every i18n_string / i18n_text field is stored as an object with the _i18n flag:

"title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" }

On every new revision the indexer walks the JSON and rebuilds a flat translation registry: item_id + field_path + locale → value. The path uses the rewards[0].title notation. Editing a translation is a versioned change: it creates a new revision of the item.

CSV pipeline for Google Sheets

The export produces a CSV with one row per translatable field and one column per locale:

collection,item_id,field_path,en,ru
bonuses,93a01703-…,title,Welcome Pack,Приветственный набор
bonuses,93a01703-…,rewards[0].title,Sword,

The file goes into Google Sheets, translators fill in the empty cells, then the CSV is imported back. Import rules:

Access model (ACL)

Four access levels, each including the previous ones:

LevelWhat it allows
viewreading records, schemas, the translation registry, export
translate+ editing translations (targeted and CSV import)
edit+ creating/editing/deleting records, rollback, status changes, asset uploads
manage+ structure (collections, schemas), members, ACL, project settings, import, publishing

Permission sources (in priority order):

  1. Super admin — full access to everything.
  2. Project role — the base level: viewer → view, translator → translate, content_manager → edit, admin → manage.
  3. Per-collection override — overrides the project role for a specific collection. The none value hides the collection completely: it disappears from listings, and any request returns 403.

Example: a content manager has none on the "Quests" collection — they can edit everything except quests. A viewer has edit on "Bonuses" — they can edit bonuses only.

CDN publishing

Each project keeps its own CDN settings in the cdn_settings field:

{
  "driver": "s3",
  "bucket": "my-rpg-content",
  "region": "eu-central-1",
  "endpoint": "https://s3.example.com",
  "base_url": "https://cdn.example.com",
  "prefix": "content/"
}

The s3 driver works with any S3-compatible storage (AWS S3, MinIO, DO Spaces, Selectel, etc., including a custom endpoint and path-style URLs). The local driver writes files to local storage — handy for development. Secrets are masked in all API responses.

Publishing (requires the manage level):

curl -X POST https://api.loreset.dev/v1/projects/1/publish \
  -H "Authorization: Bearer <token>"

What happens:

  1. The next monotonic version is allocated.
  2. A loreset/bundle-v1 bundle is built from the published records.
  3. The bundle-v{N}.json file is uploaded to the CDN via the project's driver.
  4. The versions.json index is rewritten next to it.
  5. A publication record with the URL, checksum and record count is stored in the history.

The publication history is available via GET /v1/projects/{id}/publications — sorted by version, newest first. If the upload fails, a record with the failed status is created, and such a publication never appears in versions.json.

Live example: the demo game Heliostorm (Godot 4.6) uses every mechanism described here — the bundle, versions.json and hot content updates from the CDN.