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