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.
Where it stands. Orbit runs locally only: the server instance was retired on 2026-09-11, and its source is kept on the server under /srv/stacks_disabled. The code keeps up with the engine — it is on WarpEngine 1.5.1 since 2026-10-26.
| 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.
The jump brought the engine's migrations (the category column, the status and category vocabulary, shelf_name), which a db:migrate runs, and two things the host had to do itself:
.kept everywhere. Since 0.10 the engine models have no default_scope, so a query on Software, Release, ReleaseAsset, ApplicationToken, Pipeline, PlatformLink or Download sees soft-deleted rows too. Orbit relied on the old scope in about twenty places — a revoked device token stayed on the devices page — and every one of them asks for .kept now. The privacy erasure is the exception on purpose: it anonymizes the download rows of deleted assets as well.exporter image (commodore-player) since 0.11, and since 1.5 the bevy, ebitengine and phaser web shells and the LÖVE bundle id prefix come from the platform entries, with the values the engine used to bake in.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:9140 |
| Admin | http://localhost:9140/admin |
| Mail (Mailpit) | http://localhost:8025 |
| MinIO console | http://localhost:9001 |
Every published port can be moved in .env (ORBIT_WEB_PORT and the rest), and the infrastructure listens on 127.0.0.1 only.
cgr.dev/chainguard/minio): MinIO stopped publishing images in 2025, and minio/minio is gone from Docker Hub. It is the same server built from source, :latest only, running as uid 65532 — an old root-owned volume needs docker compose down -v or a chown. minio-init creates the two private buckets with one mc mb -p, retrying until the server answers.css service keeps watching. Tailwind 4's watcher exits when stdin closes, which a container without a TTY does at once; bin/dev-css runs both bundles' watchers with [always]. A folder created while it runs is picked up after docker compose restart css.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.