What a host gets by moving up from 0.7: - `WarpEngine::Platform` — the platform registry. `PlatformLink::SUPPORTED_PLATFORMS` keeps working as an alias. - `WarpEngine::ApiErrorRendering` — the engine's JSON error bodies, includable by the host's own API controllers. - `WarpEngine::Storage.mime_for` and `ReleaseAsset.for_relative_path`. - `c.site_url` — where the catalog's public site lives, for the admin's link to a title's page. nil by default, and the link is simply left out. - `Software` now validates `platform` and `status` against their value sets. A host writing a status outside development/demo/released/archived with `update!` will start hearing about it — which is the point — and one writing it with `update_columns` still will not. - Ordering left `PlatformLink`'s and `SoftwareImage`'s `default_scope` for an `ordered` scope. The API's output order is unchanged; code that relied on the implicit order of a bare `PlatformLink.all` has to ask for it. - The generated `Image` model writes its file `after_commit` instead of `before_save`, and gained `#url`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Teletype Games
Monorepo for the Teletype Games portal: a Vue 3 frontend, a Rails 8 API host app, and the reusable WarpEngine software-catalog engine.
Project structure
apps/
frontend/ # Vue 3 + Vite + TypeScript + Tailwind SPA
api/ # Rails 8 host app: TTG-specific API + ActiveAdmin shell
libs/
ruby/warp_engine/ # WarpEngine: mountable Rails engine (catalog, updater, admin resources)
The API is split in two layers:
- WarpEngine (
libs/ruby/warp_engine) owns the software catalog: models (softwares, releases, release assets, software-image links, platform links, download stats), the CI-callable/build/*publishing endpoints, the public read-only JSON API (/api/software*,/api/builds*,/api/download,/file/*) and the catalog ActiveAdmin resources. See its README. - The host app (
apps/api) owns everything TTG-specific: members, events, the image library (Image,/api/image/:id, the admin page — the engine only links to it through an adapter), the store registry, wiki proxy, RSS feeds, Devise/ActiveAdmin authentication, theming and assets. It consumes WarpEngine as a path gem and mounts it at/.
The store registry
GET /api/stores lists the stores a client can install from. The desktop
graphical client reads it on first run, which is why it is public: a client has
nobody to log in as.
[
{ "name": "Teletype Games", "catalogUrl": "https://teletypegames.org", "storeRepositoryUrl": null }
]
A Store row has two required fields — name and catalog_url — plus an
optional store_repository_url, maintained from ActiveAdmin ▸ 🛒 Stores;
db/seeds.rb creates our own.
A store does not need a repository. The store engine's own defaults already
cover the host-to-asset mapping, the install modes, the platforms and the
behaviour; what they cannot know is identity — a slug, a name and a catalog — and
that is what this row carries. With no repository the client derives the slug from
the catalog host, writes a small config and installs. Given one, it reads that
repository's config.json and points it at catalog_url, and that file remains the
authority on how the store behaves; a repository without a config file is treated
as no repository at all.
Every WarpEngine API response also carries a WarpEngine-Version header — the registry
above is the host's own endpoint and does not, because it is not part of the engine.
This lives in the host app on purpose, not in WarpEngine. The engine serves one catalog and has no business knowing which stores exist for it; who ships a store for a catalog is a property of the site.
| Model | apps/api/app/models/store.rb |
| Endpoint | apps/api/app/controllers/api/stores_controller.rb |
| Admin | apps/api/app/admin/stores.rb |
| Client | warp-engine-client |
Development environment
docker compose up -d
| Service | URL |
|---|---|
| Frontend | http://${WEBAPP_DOMAIN} |
| API | http://${WEBAPP_DOMAIN}/api |
| Admin | http://${WEBAPP_DOMAIN}/admin |
| API docs | http://${WEBAPP_DOMAIN}/api/swagger |
The api image is built from the repo root (so the libs/ path gems are visible
during bundle install) and mounts ./libs at runtime. After changing the
compose file or the Gemfile, rebuild with docker compose up -d --build api.
Testing
make api-test
Runs the host suite (apps/api, softwares_test DB), the WarpEngine suite
(dummy app, warp_engine_test DB) and a production-mode zeitwerk:check.
Individually:
docker exec -e RAILS_ENV=test api bundle exec rspec # host
docker exec -w /libs/ruby/warp_engine -e RAILS_ENV=test api bundle exec rspec # engine
JSON contract baselines for /api/software and /api/builds live in
apps/api/spec/snapshots/ — diff against them after refactors.
Linting
Backend (RuboCop)
docker compose run --rm --no-deps api bundle exec rubocop # check
docker compose run --rm --no-deps api bundle exec rubocop -A # autofix
Config: apps/api/.rubocop.yml (rubocop-rails-omakase preset)
Frontend (ESLint)
docker compose run --rm --no-deps frontend npm run lint # check
docker compose run --rm --no-deps frontend npm run lint:fix # autofix
Config: apps/frontend/eslint.config.js (ESLint 9 flat config, Vue + TypeScript)