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.
| 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.
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.
Orbit configures WarpEngine's device authorization grant, so the desktop WarpEngine Client can hold an account:
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.
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.
Everything is in Compose, app and Sidekiq worker included:
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.The app follows Growing Rails Applications in Practice. Two rules do most of the work:
DeviceApprovalsController#create, not DevicesController#approve. A download is a resource whose creation issues the grant.The full plan and the reasoning behind the decisions are in NOTES_TELETYPE_ORBIT.md in the repository.