TITLE=Teletype Orbit ROUTE=/services/teletype-orbit LOCALE=en MODIFIED=1792409344 URL=https://wiki.teletypegames.org/services/teletype-orbit L=1 #22 L=2 #46 q│ Teletype Orbit is a game shop like itch.io or Steam: a Rails 8 + Hotwire q│ application built on the 1WarpEngine catalog engine. WarpEngine owns the q│ catalog — software, releases, artifacts, CI publishing. Orbit owns what a q│ shop needs on top: price, order, entitlement, publisher, moderation, payout. 1What it adds to WarpEngine 1══════════════════════════ t┌────────────┬───────────────────────────────────────────────────────────────┐ t│h Area t│h What Orbit brings t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bShop0 t│0 catalog pages, search, filters, product page, cart, checkout, t│ t│0 t│0 orders t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bLibrary0 t│0 entitlements, gated downloads, keys, gifts t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bPlay0 t│0 web-playable builds behind an entitlement check t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bStudio0 t│0 publisher portal: product, price, store page, images, t│ t│0 t│0 channels, publishing tokens, hosted builds t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bModeration0 t│0 submissions, reports, repository approval t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bCommunity0 t│0 reviews, forum, wishlist, following, activity t│ t├────────────┼───────────────────────────────────────────────────────────────┤ t│0 bFinance0 t│0 append-only ledger, revenue share, payouts, invoices t│ t└────────────┴───────────────────────────────────────────────────────────────┘ The engine's words stay the engine's. A cSoftware0 is the technical catalog entry, a cRelease0 is a build version. The sellable thing is a cStore::Product0, and the customer-facing build pointer is a cStudio::Channel0. Only an id links the two, with no foreign key — the engine soft-deletes, so its rows can vanish underneath. 1The download gate 1═════════════════ bBefore.0 WarpEngine's c/api/download0 and c/file/*0 did no authentication, so Orbit shadowed both with routes placed before cmount WarpEngine::Engine0. It worked, but it depended on route order — a defence that breaks silently when a line moves. bNow.0 Since WarpEngine 0.5.0 the engine asks an 1access policy first, and Orbit's is cLibrary::AccessPolicy0: t┌────────────────────────┬───────────────────────────────────────────────────┐ t│h Method t│h What it does t│ t├────────────────────────┼───────────────────────────────────────────────────┤ t│0 cvisible_software_scope0 t│0 narrows cGET /api/software0 to t│ t│0 t│0 cStore::Product.listed0, so drafts, in-review, t│ t│0 t│0 delisted and private products are not listed t│ t├────────────────────────┼───────────────────────────────────────────────────┤ t│0 caccess_for0 t│0 attaches the price, the purchase page, and t│ t│0 t│0 whether ithis0 caller owns it t│ t├────────────────────────┼───────────────────────────────────────────────────┤ t│0 cauthorize_download0 t│0 runs the rules cLibrary::DownloadService0 already t│ t│0 t│0 had: entitlement, rate limit, grant, download t│ t│0 t│0 event t│ t└────────────────────────┴───────────────────────────────────────────────────┘ c/api/download0 is no longer shadowed. c/file/*0 still is, on purpose: a web build runs in a bbrowser0, 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. cspec/requests/library/download_gate_spec.rb0 is the regression test, and it is not optional. 1Signing in from a client 1════════════════════════ Orbit configures WarpEngine's device authorization grant, so the desktop 2WarpEngine Client can hold an account: p c.access_token_owner_class = "Accounts::User" p c.identity_verification_url = "/devices" p 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: t┌────────────────┬───────────────────────────────────────────────────────────┐ t│h Page t│h What it does t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 c/devices0 t│0 a person types the short code the client shows, sees t│ t│0 t│0 which device is asking, approves or refuses t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 c/client_tokens0 t│0 the devices signed in to this account, and removing one t│ t└────────────────┴───────────────────────────────────────────────────────────┘ csubject_resolver0 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. bA client is told what it needs and nothing more.0 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. 1One Orbit-side bug worth remembering 1════════════════════════════════════ Every cWarpEngine::Software0 was created with status c"development"0 and nothing moved it, because nothing in Orbit reads that field — its pages ask the product. A bcatalog client does0 read it: the desktop store lists released and archived titles and skips the rest, so the whole catalog was invisible to it. cStore::Product0 now keeps the two in step (cpublished → released0, cdelisted → carchived0, demo product → cdemo0) with an cafter_save0, not a line in a service, because the state changes from the moderation flow and from ActiveAdmin alike. 1Running it 1══════════ Everything is in Compose, app and Sidekiq worker included: p cp .env.example .env p make up p docker compose exec app bin/rails db:seed t┌────────────────┬─────────────────────────────┐ t│h Surface t│h URL t│ t├────────────────┼─────────────────────────────┤ t│0 Shop t│0 http://localhost:3000 t│ t│0 Admin t│0 http://localhost:3000/admin t│ t│0 Mail (Mailpit) t│0 http://localhost:8025 t│ t│0 MinIO console t│0 http://localhost:9001 t│ t└────────────────┴─────────────────────────────┘ Seeding has two levels: • cmake seed-core0 — reference data that also runs in production: genres, tags, EU VAT rates, revenue shares, forums. • cmake seed-examples0 — development-only samples. They go through the breal0 path (Studio → submission → approval → cart → payment webhook), so the ledger and the reports hold meaningful data. 1Conventions 1═══════════ The app follows iGrowing Rails Applications in Practice0. Two rules do most of the work: • bA controller is trivial and has only standard actions.0 Approving a device is cDeviceApprovalsController#create0, not cDevicesController#approve0. A download is a resource whose creation issues the grant. • bLogic lives in services0, 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 cNOTES_TELETYPE_ORBIT.md0 in the repository.