EN /HU | Login

Public site

The Teletype Games public platform: one monorepo (services/teletypegames) with the catalog backend (Catalog API) and the retro-styled frontend, run by Docker Compose.

Monorepo layout

TXT
apps/
  frontend/   # Vue 3 + Vite + TypeScript + Tailwind
  api/        # Rails 8 API + ActiveAdmin

docker compose up -d starts the dev environment. Both apps sit under one domain:

Path Serves
/ frontend
/api Catalog API
/admin ActiveAdmin
/api/swagger API docs

Linting: RuboCop (rubocop-rails-omakase) for the backend, ESLint 9 (flat config, Vue + TypeScript) for the frontend.

Catalog API

Rails 8 + MySQL. ActiveAdmin panel (blaze theme, Devise auth), Blueprinter serializers, apipie-rails docs with a browsable Swagger UI. It is the single source of truth for software metadata, releases, members, events and download logs, and it serves images and binaries.

Domain model

Software (name, title, author, desc, story, license, platform, status, highlighted; soft-deleted) has many Releases and ExternalLinks, and images through SoftwareImage (default flag + ordering). A Release holds the version and the cartridge / source / HTML folder / docs folder paths. Every download is a Download row (IP, user agent, referer, release), surfacing as downloadCount / totalDownloads in API responses.

Endpoints

Endpoint Purpose
GET /api/software Catalog listing: software + sorted releases + latestRelease (skipping dev-) + webPlayableRelease + totalDownloads
GET /api/software/highlighted Featured software
GET /api/events Community events
GET /api/members Team members
GET /api/stores Store registry (see below)
GET /api/image/:id Image serving
GET /api/wiki/pages?tag=blog\|howto[&limit][&body=1][&lang=en\|hu] Wiki proxy: fetches Grav's custom/pages.json via WIKI_GRAV_URL, so the frontend never talks to the wiki. lang picks the wiki language, defaulting to en
GET /api/rss/blog RSS of blog-tagged wiki pages
GET /api/rss/releases RSS of the latest releases
GET /api/download?path=… Logged download (records IP / UA / referer, then serves)
GET /api/swagger apipie docs
GET /file/*path Direct file download; release paths in API responses point here
POST /build/upload Artifact upload, one file per request; application token with the upload scope
POST /build/publish Release publishing; application token with the update scope, called by CI after upload

Tokens are described in the WarpEngine article.

The store registry (GET /api/stores) says which catalogs a client can install from and where each store's config lives. It is public, because the desktop client reads it on first run and has nobody to log in as. A row is three fields (name, catalog_url, store_repository_url), edited in ActiveAdmin ▸ 🛒 Stores. It lives in the host app, not in WarpEngine: the engine serves one catalog and does not know who ships stores for it.

Publish pipeline

/build/publish dispatches by platform to a Platforms service (tic80, love, ebitengine, c64, plus4, godot, bevy, phaser). Each one picks up the uploaded artifacts (C64 and Plus/4: <name>-<version>.prg + <name>-<version>.metadata.json, the EmulatorJS .html.zip and the yapeSDL -linux-x64, -win-x64 and -mac-universal zips), upserts the Software from the metadata, adds a Source Code external link from its site field, and creates the Release. The Gitea + Woodpecker side is in the Deployment article.

Frontend

A Vue 3 SPA built with Vite in TypeScript: vue-router (one module per page), Pinia stores per domain (software, events, members, git, blog, howtos, youtube, ui), vue-i18n, and Tailwind CSS + TuiCSS for the retro terminal look. Traefik proxies it on the same base domain as the API.

Pages: home, catalog (index + show), blog, code, howtos, team, contact. The catalog show page has an image lightbox, the release list, download counts and web-playable releases.

One client module per source under src/api/:

Module Source
software.api.ts, event.api.ts, member.api.ts Catalog API
wiki.api.ts Catalog API wiki proxy (blog / howto tags), passing the interface language as lang. VITE_WIKI_BASE is only used to build external links to Grav
git.api.ts Gitea REST API v1 (token WEBAPP_GITEA_TOKEN) for the Code page
youtube.api.ts YouTube Data API: latest channel video and view count

Analytics. useMatomo sets the Matomo tracker up once and counts a page view only in the router's afterEach, so a visit is one page view (it used to be two). The first navigation keeps the external referrer instead of overwriting it with the site's own /. Tracking is cookieless (disableCookies, no _pk_id, no consent needed), and a heartbeat timer records the time spent on single-page visits too. The Gitea header carries the same tracker.

CI

Workflow Runs on What it does
.woodpecker/deploy.yaml push to master, manual rubocop and test-host in parallel, then deploy waits for both; deploy is an SSH call to ci-deploy teletypegames on the server
.woodpecker/warp_engine.yaml changes under libs/ruby/warp_engine version checks, rubocop + test-engine, then the split mirror to engines/warp_engine and the gem publish
.woodpecker/ci-images.yaml ci/** or gem-defining files on master, manual, nightly cron builds the CI images below

No step gets a host folder or the docker socket, so nothing needs trusted: volumes (the old /srv/ci-cache volume is gone): the tools and gems live in our own CI images, built from ci/*.Dockerfile with kaniko (no privileged mode, no docker socket) and pushed to the Gitea registry under git.teletypegames.org/build/…:

Image Used by
teletypegames-ci:api-ruby3.3 deploy.yaml rubocop + test-host (ruby 3.3, mysql2 build deps, the apps/api gems)
teletypegames-ci:warp_engine-ruby3.2 warp_engine.yaml rubocop + test-engine
deploy-ssh:alpine3.22 every ssh deploy@ deploy step of every repo, referenced by digest
dos-ci:bookworm the test step of the dos/* repos (Open Watcom, DOSBox, DOSBox Staging, Wine, clang-format)
ci-tools:alpine3.22 helper steps: git, git-subtree, jq, curl, zip, lua, luacheck, ldoc — WarpEngine's tools image for every game pipeline (by digest), the release steps, the split mirror
pyftpdlib:3.13 the test step of infra/ftp-server
warp-engine-client-ci:node22 the check step of stores/warp-engine-client, with its node_modules preinstalled
ebitengine-ttg:latest WarpEngine's ebitengine builder: ebitengine-builder plus the Go modules of every TTG Go project
bevy-ttg:latest WarpEngine's bevy builder: bevy-builder plus the crates of every TTG Rust project

Every image gets a moving tag and a commit-bound one (…-<commit>), and is rebuilt only when its Dockerfile or the files that decide its gems change on master, or by hand. The exceptions are warp-engine-client-ci, ebitengine-ttg and bevy-ttg, whose dependencies live in other repos: the nightly ci-images-nightly cron looks at them too, and builds only when there is no image yet for the current dependencies (lock-<sha> for the client's package-lock.json; deps-<sha> for the go.mod/go.sum and Cargo.toml/Cargo.lock files that ci/collect-deps.sh gathers from every branch of the games, demos and engines orgs, plus the builder's digest). A new dependency still downloads in the build as before. The workflow needs one secret, forge_token (package:write).