EN /HU | Login

Teletype Orbit

Teletype Orbit is a game shop like itch.io or Steam: a Rails 8 + Hotwire application built on the WarpEngine catalog engine. WarpEngine owns the catalog — software, releases, artifacts, CI publishing. Orbit owns what a shop needs on top: price, order, entitlement, publisher, moderation, payout.

What it adds to WarpEngine

Area What Orbit brings
Shop catalog pages, search, filters, product page, cart, checkout, orders
Library entitlements, gated downloads, keys, gifts
Play web-playable builds behind an entitlement check
Studio publisher portal: product, price, store page, images, channels, publishing tokens, hosted builds
Moderation submissions, reports, repository approval
Community reviews, forum, wishlist, following, activity
Finance append-only ledger, revenue share, payouts, invoices

The engine's words stay the engine's. A Software is the technical catalog entry, a Release is a build version. The sellable thing is a Store::Product, and the customer-facing build pointer is a Studio::Channel. Only an id links the two, with no foreign key — the engine soft-deletes, so its rows can vanish underneath.

The download gate

Before. WarpEngine's /api/download and /file/* did no authentication, so Orbit shadowed both with routes placed before mount WarpEngine::Engine. It worked, but it depended on route order — a defence that breaks silently when a line moves.

Now. Since WarpEngine 0.5.0 the engine asks an access policy first, and Orbit's is Library::AccessPolicy:

Method What it does
visible_software_scope narrows GET /api/software to Store::Product.listed, so drafts, in-review, delisted and private products are not listed
access_for attaches the price, the purchase page, and whether this caller owns it
authorize_download runs the rules Library::DownloadService already had: entitlement, rate limit, grant, download event

/api/download is no longer shadowed. /file/* still is, on purpose: a web build runs in a browser, where the credential is the session and the right answer to "not yours" is a redirect to login, not a bare 403. The engine has neither.

The risk changed shape rather than disappearing — a policy can fail to be wired up as silently as route order could. spec/requests/library/download_gate_spec.rb is the regression test, and it is not optional.

Signing in from a client

Orbit configures WarpEngine's device authorization grant, so the desktop WarpEngine Client can hold an account:

RUBY
c.access_token_owner_class  = "Accounts::User"
c.identity_verification_url = "/devices"
c.subject_resolver = ->(request) { request.env["warden"]&.user(:user) }

The engine owns the flow. The two pages are Orbit's, because approving needs a session and a logged-in human:

Page What it does
/devices a person types the short code the client shows, sees which device is asking, approves or refuses
/client_tokens the devices signed in to this account, and removing one

subject_resolver is the other half: a browser carries a session, not a bearer token, and without it the engine would refuse Orbit's own signed-in visitors.

A client is told what it needs and nothing more. Price, ownership, where to buy, whether sign-in exists — all of it comes from the engine's generic contract, so the client carries no Orbit-specific code. One client must serve any WarpEngine catalog; a rule compiled into it would break every other one.

One Orbit-side bug worth remembering

Every WarpEngine::Software was created with status "development" and nothing moved it, because nothing in Orbit reads that field — its pages ask the product. A catalog client does read it: the desktop store lists released and archived titles and skips the rest, so the whole catalog was invisible to it. Store::Product now keeps the two in step (published → released, delisted → archived, demo product → demo) with an after_save, not a line in a service, because the state changes from the moderation flow and from ActiveAdmin alike.

Running it

Everything is in Compose, app and Sidekiq worker included:

SH
cp .env.example .env
make up
docker compose exec app bin/rails db:seed
Surface URL
Shop http://localhost:3000
Admin http://localhost:3000/admin
Mail (Mailpit) http://localhost:8025
MinIO console http://localhost:9001

Seeding has two levels:

  • make seed-core — reference data that also runs in production: genres, tags, EU VAT rates, revenue shares, forums.
  • make seed-examples — development-only samples. They go through the real path (Studio → submission → approval → cart → payment webhook), so the ledger and the reports hold meaningful data.

Conventions

The app follows Growing Rails Applications in Practice. Two rules do most of the work:

  • A controller is trivial and has only standard actions. Approving a device is DeviceApprovalsController#create, not DevicesController#approve. A download is a resource whose creation issues the grant.
  • Logic lives in services, one public method per use case, with the specs weighted there: every branch, every failure path.

The full plan and the reasoning behind the decisions are in NOTES_TELETYPE_ORBIT.md in the repository.