docs play ▸

Architecture

typepper is a typing game that runs in a terminal and in a browser, plus a small server for sharing replays and keeping leaderboards. This page is the map; each part has its own doc.

Principles

  1. Offline first. The game never needs the network. Nothing is sent at startup or while playing; the first possible contact is a finished round, and only because the player asked (pressing u, opening the boards, or having linked their account). A network failure is one dim line of text, never a dialog, and never blocks a key. settings → online: off turns every online feature and hint off.
  2. One set of rules. The game, the browser and the server run the same engine crate, and it's deterministic: the same start and the same keys give the same round, on any machine, at any frame rate.
  3. The server trusts no score. A run is uploaded as its inputs, never its result; the server plays it again and keeps its own numbers.
  4. Small, boring infrastructure. One service and one database on Railway, everything declared in the repo, deployed with each release.

Components

flowchart LR
  subgraph players
    T["terminal game<br/>(runs the system ssh)"]
    W["web game<br/>typepper.com"]
    F["anyone with a link<br/>r.typepper.com/x7Kp2QaZ"]
  end
  subgraph railway["Railway, project typepper"]
    S["typepper-server<br/>HTTP (axum) + SSH (russh)"]
    PG[("typepper-postgres")]
  end
  GH["GitHub<br/>(public keys, user ids)"]
  T -- "ssh.typepper.com:36080<br/>submit · link · board" --> S
  W -- "https://api.typepper.com<br/>submit · replays · boards" --> S
  F -- "share page" --> S
  S -- "redirect to typepper.com/?replay=…" --> W
  S --> PG
  S -. "linking only" .-> GH
HostServesBehind
typepper.comthe web game (static: wasm, fonts, installer script)GitHub Pages
api.typepper.comthe HTTP APIRailway's edge (port 8080), Cloudflare proxied
r.typepper.comshare pages (same service)Railway's edge (port 8080), Cloudflare proxied
ssh.typepper.comthe SSH door, port 36080Railway's TCP proxy (sakura.proxy.rlwy.net:36080 → 2222), DNS only
docs.typepper.comthese docs (static, its own service)Railway's edge (port 8080), Cloudflare proxied

Railway picks the TCP proxy's port, and released games have it built in, so the proxy must never be recreated (see operations).

The repository

Cargo.toml                 # virtual workspace; every crate shares its version
crates/
├── typepper/              # the game: ratatui UI for the terminal (crossterm) and the web (ratzilla)
├── typepper-engine/       # the rules: no IO, no UI; used by the game and the server
└── typepper-server/       # HTTP + SSH over Postgres; deployed, never released
infra/
├── railway/railway.ts     # the Railway project, as code
├── docker/                # the server's and the docs' images
└── ssh/                   # the server's public host key (the game ships it)
docs/                      # these docs: play/, online/, reference/, built/
site/                      # docs.typepper.com: Zola templates, and prepare.py (docs/ → content)
index.html, web/, assets/  # the web page around the game, and its assets
demo/                      # recording the README's GIFs
mise.toml, mise.lock       # pinned tools and tasks
rust-toolchain.toml        # pinned Rust
CrateDepends onShips as
typeppertypepper-engine, ratatui, crossterm / ratzillabinaries for 5 platforms (cargo-dist), the web build (Trunk)
typepper-enginelibm, web-timepart of the other two
typepper-servertypepper-engine, axum, russh, sqlx, ureqa Docker image on Railway

cargo build, cargo run and cargo test mean the game (default-members); add --workspace or -p <crate> for the rest.

How a round travels

sequenceDiagram
    participant P as player
    participant G as game
    participant S as server
    participant DB as postgres
    P->>G: keys
    Note over G: the engine logs each input at its millisecond
    G->>G: round over: results screen, last.replay saved
    alt linked (typepper link)
        G->>G: the round goes into the outbox
        G->>S: v1 submit (the whole outbox)
    else u pressed
        G->>S: v1 submit (this round, anonymously)
    end
    S->>S: decode, play it again (simulate)
    S->>DB: the bytes and the server's own score
    S-->>G: ok <id> <score> <rank> <link>
    G-->>P: "saved: r.typepper.com/… · #3 all time"

Watching goes the other way: a share link's page sends the browser to typepper.com/?replay=<id>, and the game fetches the bytes and plays them through the same engine and the same screens, in real time.

Trust

Versions

Everything shares the release version (0.4.0): the game shows it on the title screen, the server reports it in /health, and a release tag ships the binaries, the site and the server together. Three things are versioned on their own, each for its own reason:

WhatVersionChanges when
the protocol's commands/v1, v1requests or answers change shape
the replay bytesformat (1)the byte layout changes
the rulesengine::VERSION (1)a change would alter results