The desktop graphical client had our store's config URL compiled into it. That is backwards: which stores exist for a catalog is something the site knows, and a client should be able to ask. `GET /api/stores` answers, and the client picks from what comes back. A `Store` row is three fields — name, catalog_url, store_repository_url — with an ActiveAdmin panel, a Blueprinter serializer in the catalog's camelCase, and a service+controller pair following the events and members shape. `db/seeds.rb` creates our own record, so a fresh database serves a working registry. The endpoint is **public**, which is the point: the client runs on someone's laptop before any store exists and has nobody to log in as. It exposes three URLs that are public anyway. This deliberately does not live in WarpEngine. The engine serves one catalog and has no business knowing who ships stores for it; a registry of stores is a property of this site, not of the catalog software. Anyone mounting WarpEngine can keep their own list, or none. 16 model and controller examples pass. Worth noting for the next person who runs them: the specs need RAILS_ENV=test, as the README says — without it rspec runs in the development environment, host authorization rejects Rack::Test's hostname, and every request spec fails with a 403 and an HTML body that looks nothing like a routing problem. 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, images, platform links, download stats), the CI-callable/build/*publishing endpoints, the public read-only JSON API (/api/software*,/api/builds*,/api/image,/api/download,/file/*) and the catalog ActiveAdmin resources. See its README. - The host app (
apps/api) owns everything TTG-specific: members, events, 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": "https://git.teletypegames.org/stores/ttg-desktop-store"
}
]
A Store row is three fields — name, catalog_url, store_repository_url —
maintained from ActiveAdmin ▸ 🛒 Stores, and db/seeds.rb creates our own.
The client derives everything else: it reads config.json from the store
repository, points it at catalog_url, and falls back to the engine's defaults if
that repository has no config file.
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-desktop-gui |
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)