---
title: 'Public site'
url: 'https://wiki.teletypegames.org/services/public-site'
markdown: 'https://wiki.teletypegames.org/services/public-site.md'
lang: en
date: '2026-07-28'
description: '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 apps/ frontend/ # Vue 3 + Vite + TypeScript + Tailwind api/ # Rails 8 API + ActiveAdmin docker compos…'
taxonomy:
  tag:
    - howto
---

# Public site

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

# Monorepo layout

```
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 `Release`s and `ExternalLink`s, 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](https://wiki.teletypegames.org/engines/warp-engine) 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`).

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [Services](https://wiki.teletypegames.org/services.md)
- Previous: [Pixelia BBS](https://wiki.teletypegames.org/services/pixelia-bbs.md)
- Next: [Scriptorium BBS](https://wiki.teletypegames.org/services/scriptorium-bbs.md)
