devlog.

Project · Web service

TerminalGB Portal

Trade a Gen 1 Pokémon with a friend over the web, with one Game Boy each and no appointment. Both players trade against their own local link partner in their own time; the portal carries a description of the Pokémon between them and never a byte of game data.

~250 B
On the wire, per Pokémon
0
Game bytes, saves or ROMs
65
End-to-end checks, green
$0
Per month, at hobby scale

Checks run 14 Aug 2026 against a local Postgres 16 and a real Chrome.

PLAYER A · ANY TIME PLAYER B · ANY LATER TIME cartridge A’s own save virtual partner in the emulator link cable cartridge B’s own save virtual partner in the emulator link cable THE PORTAL a PokemonSource over HTTPS two specs and their timestamps POST ~250 B GET ~250 B no session · no lobby · no synchronisation Never crosses the wire: a save file, a ROM, or a single byte of game data. The whole per-trade dataset is two ~250-byte specs and their timestamps.
The v1 trade. Each player's cable is answered by their own local virtual partner; the portal never joins two cables together and never sees a link byte. What crosses is a description — species, level, original trainer, moves, determinant values.

What it does

Trading

Nobody waits for anybody

  • One player deposits a Pokémon; the other collects it whenever they next sit down. No session, no lobby, no presence
  • Offers are addressed to named friends, are irreversible on accept with a confirmation, and auto-withdraw
  • A trade log both sides can read
  • Every incoming spec is validated against Gen 1 tables — species, moves, level, determinant values, name character sets — at deposit, and re-resolved by the receiving client against its own cartridge's tables

Collections

Sprites for the session only

  • A friend's party and boxes as their own terminal reported them, sprites included, from their own emulator running their own cartridge
  • Held for the session. When the terminal disconnects they are purged. No sprite store, no server-side fallback, no reusing one user's picture for another user's Pokémon of the same species
  • A disconnected user shows no sprite at all. That is the correct behaviour, not a degraded one
  • An offer whose Pokémon evolved has its stored picture nulled: a wrong picture is worse than none

Accounts

Sign in on a device with no browser

  • Sessions belong to the authentication provider; nothing hand-rolled
  • The CLI gets opaque bearer tokens — 256 bits of randomness, stored only as a SHA-256 hash, 30-day access and 90-day refresh, with rotation and reuse-detection revocation
  • Device codes expire in ten minutes, are single-use, and enforce their polling interval with per-IP and per-user rate limits. Approval requires a signed-in browser session, never a bearer token
  • Friend codes are cryptographically random from a 30-symbol alphabet, and the lookup endpoint returns one generic error for every failure mode, so codes cannot be enumerated

Language

Gen 1 has no shiny Pokémon

  • What Gen 1 has is determinant values, from which Gen 2 derives both shininess and Hidden Power. So every surface reads "would be shiny if traded to Gen 2", never "is shiny"
  • The portal recomputes the prediction from the determinant values itself, against the published Gen 2 rule, and never trusts the flag on the wire
  • The Hidden Power line is the one line in a card allowed to wrap to two, because the full phrase does not fit. Wrapping varies a card's height, never its width

What is tested, and what is not

Two lists rather than one status badge.

Tested, observed workingWritten but not exercised
All 65 end-to-end checks green: the full friend flow; offer, accept, done and log through both cookie and bearer authentication; the complete device-code flow including authorization_pendingslow_down → approval → single-use consumption; token refresh rotation with reuse revocation; non-enumeration; atomic double-accept rejection; rate limiting. The authentication provider with real keys. No account was available in the build environment; everything follows the current major version's documentation and builds keyless, but the first deploy must smoke-test sign-in before anything else.
A browser walk in real Chrome: dev sign-in, friend request and accept, the offer form, the accept flow with its irreversibility confirmation, mark-done, the code approval page, the trade log. The production build passes with and without the dev auth stub. The hosting and database deployment. Standard patterns, never run from here.
The collection browser walked in both states: live, with canvas sprites from a synthetic test payload and the filters exercised; and offline, with sprites purged, the listing intact and a last-seen shown. Time-based expiries — device code at ten minutes, tokens at 30 and 90 days. Enforced in SQL, but no test waits them out.

Not in v1, and the interface says so

Written into the product rather than into a roadmap document, which is the difference between a scoped v1 and a v1 that is quietly missing things.

Live link sessions Battles The browser as a primary client Strangers · public lobby · matchmaking Multi-Pokémon trades Presence beyond a friends-only badge Gen 2 and 3 Mobile

Limits worth knowing

Writing


Where these numbers come from

The test counts and their date are the repository's own recorded observations, kept in a section that separates what was run from what is standard practice. The six relay checks were verified green twice on 14 Aug 2026. The diagram is drawn from the shipping v1 architecture and the shared HTTP contract.

Pokémon, Game Boy and Nintendo are trade marks of their respective owners. This portal transfers a description of a Pokémon and never a byte of game data.

← All projects