Wanjohi 04210cc9f1 refactor(auth): describe sign-in screens as data, draw them in one place (#339)
Providers stop returning `Response`. Each says what it needs from the
person — an address, a pin, a yes-or-no — as a `Screen`, and one
`Renderer` draws it.

The old shape made every provider a small web framework: it had to know
about markup, about the stylesheet's attribute names, about how a page
is assembled. So each grew its own callback signature, its own copy, and
its own `new Response(jsx.toString())` — and there was no shared
vocabulary left to style.

### What that had already cost

- **The device flow never got a design.** Its two pages were built by
concatenating HTML strings, with an inline `style` on the user code. Six
more replies were `text/plain` — including the one a person gets when
their sign-in cookie expires.
- **The password screens were already broken.** They render
`data-component="input"`, `data-component="link"`,
`data-component="form-footer"` against a stylesheet that was rewritten
for the code flow. Nobody noticed because password sign-in is not
switched on. They are deleted here, not repaired.
- **A provider could not be named or marked without editing the
library.** `DiscordProvider` has existed all along; wiring it up
rendered **"Continue with discord"**, lowercase, no icon, because the
marks and display names were two hardcoded `const` records inside the
code that drew the chooser.

### What changed

`ui/screen.ts` — four screen kinds (`choose`, `form`, `confirm`,
`message`) and a field vocabulary, as plain data. No JSX, no hono.

`ui/render.tsx` — the only file that knows what a button looks like.
`Renderer` is one method, so replacing the presentation layer wholesale
means implementing that and nothing else.

`Provider.display` — each provider carries its own name and mark
(`ui/mark.ts`, raw SVG strings so `provider/*.ts` never imports a
rendering library). The chooser is built from what providers declare;
`issuer({ chooser })` is left with only the two decisions a deployment
makes that a provider cannot — whether to offer it, and what to put
first.

The theme global is gone. It was `globalThis`, with a comment conceding
as much: every component depended on something invisible at the call
site, untestable in isolation, and shared mutable state on a runtime
that keeps one module instance across requests. It is a closure argument
now, and `Theme` shrinks to the values a deployment sets that the
stylesheet cannot.

`kind: 'segments'` is named by meaning rather than widget — a code read
off one screen and typed into another. The emailed pin and the device
user code are the same field now; they used to describe it separately,
in different files.

### Adding things, after

```ts
providers: {
  code: CodeProvider({ ... }),
  discord: DiscordProvider({ clientID, clientSecret }),   // icon and name included
  password: PasswordProvider(PasswordUI({ ... })),        // in the design, because it has none of its own
}
```

Neither touches CSS. Neither touches the renderer.

### Notes

- **No Tailwind.** I floated it and then dropped it: `packages/auth`
deploys straight from `src/` both ways (`wrangler.jsonc` points `main`
at `src/index.ts`; `server.ts` runs the same handler under Bun), so
adding a CSS toolchain would fight both. The stringly-typed
`data-component` problem is solved by the typed components instead —
nobody adding a screen writes one. The stylesheet grew ~95 lines for the
new primitives and that is the last CSS this change needs.
- **Design tokens stay independent** of the website rather than shared,
since auth is a separate Worker on a separate hostname with its own
release cadence. The brand values are copied, with a comment naming the
site as the source. Easy to reverse if you'd rather couple them.
- `src/provider/oauth2.ts` has a pre-existing `TS2578: Unused
'@ts-expect-error'` on `dev`. Untouched — verified it fails the same way
without this branch.

### Verification

- `packages/auth`: 66/66 pass. The 27 device tests are **unmodified**
and still pass, which is the behaviour argument — statuses, cookies and
the confirmation step are unchanged.
- `apps/auth`: 23/23 pass.
- `tsc --noEmit` clean on both, apart from the pre-existing error above.
- Every screen rendered and eyeballed in a browser.

Net −757 lines.
2026-09-17 22:52:42 +00:00
2026-09-14 14:45:13 +03:00
2026-09-14 14:45:13 +03:00
2026-08-06 22:13:51 +03:00
2026-08-06 22:13:51 +03:00
2026-08-26 17:58:58 +03:00
2026-08-06 22:13:51 +03:00
2026-08-06 22:13:37 +03:00
2026-08-06 22:32:33 +03:00

Nestri logo

Run your games on a GPU you don't own — or one you do. Nestri puts an interactive workload in a hardware-accelerated virtual machine and streams it to you over QUIC, at a latency that lets you play rather than watch.

Note

This repository is mid-rewrite, and the documentation is behind the code. The guest-side components arrived recently and their docs are thin. Nothing here is stable yet: expect directories to move and interfaces to change. Proper documentation is on the way — issues and questions are welcome in the meantime, and are genuinely useful for deciding what to write first.

Try it now — nesdoctor

One thing here is finished and runs on its own machine, today:

# Linux and macOS
curl -fsSL https://doctor.nestri.io/install.sh | sh

# Windows
powershell -c "irm https://doctor.nestri.io/install.ps1 | iex"

It tells you whether your machine could host games for other people, and measures the number that actually decides whether streaming a game feels right — not your download speed, but how much latency your connection adds when it is busy. A 500 Mbps uplink that queues for 300 ms under load cannot carry a game; a 25 Mbps one with fq_codel can. Almost nobody has seen their own figure.

  upstream             35 Mbps
  latency, idle floor  56 ms
  latency, loaded     185 ms
  added under load   +129 ms   grade F

  presentation path   x11 · bspwm
  eDP-1               1920x1200 @ 60 Hz, 8-bit
  Vulkan decode       h264, h265

It also reads your display out of its EDID — resolution, refresh, colour depth, HDR transfer functions, BT.2020, chroma — and what your hardware can decode. Those decide what is worth sending over the wire, and we would otherwise be guessing from one panel in one room.

It does not stream a game. It is the piece that has to exist before anything else can, and most machines will come back CLIENT — which is a real answer, not a failure.

Downloads one binary, verifies its checksum, runs it, deletes it. Installs nothing, needs no administrator rights, touches no system directory. Nothing is uploaded: it prints a link, lists exactly what the link contains, and opens it only if you press Enter. The scripts those URLs serve are apps/nesdoctor/install/ in this repository, so you can read them before you run them.

Source and the full story: apps/nesdoctor.

What is here

Two halves that meet over the network and share very little else, plus one thing that runs on your own machine.

The control plane — TypeScript

apps/api The public REST API. Identity, teams, machines, games, pairing.
apps/auth A self-hosted OpenAuth issuer — Steam and SSH-key login.
packages/core The domain: every table, every operation, no HTTP.
packages/auth Shared auth types and subjects.

Postgres for state. Both run on Cloudflare Workers today and as ordinary containers wherever you like — one handler each, no infrastructure-as-code, and a Dockerfile in each app. See docs/deploy.md and docs/dns.md.

The guest — Rust, inside the box

These run inside a virtual machine, beside the game. None of them talk to the control plane.

apps/nescope A headless Wayland compositor for one fullscreen client. A lighter answer to the same problem gamescope solves.
apps/nescapture A Vulkan implicit layer. It captures frames from inside the workload's own process and encodes them on the GPU that drew them — no copy out to the CPU and back.
apps/neswire Audio capture and transport.
apps/neshub One connection out of the box. Muxes video, audio, cursor and input into a single QUIC stream to the client.
crates/nesprotocol The wire types they all share, so no two ends can drift apart silently.

On your own machine — Rust

apps/nesdoctor Whether a machine can host a box, and what its connection and display can really do. The first executable form of our host requirements — until it existed, a host was qualified by a human reading a table. Four dependencies; everything that could be done with the standard library is.

The hypervisor the guest components run under is nesbox, a separate repository: a micro-VM with a real GPU in it, using virtio-gpu native context rather than passthrough, so one card can host several boxes at once.

Why a virtual machine

A container shares the host kernel, which makes strong isolation hard and a GPU harder. A micro-VM boots in about as long, isolates properly, and — with native context — gets close to bare-metal graphics. That choice is what makes "many sandboxes, one GPU" possible instead of one tenant per card.

Getting started

bun install
cp .env.example .env         # compose reads every credential from here
docker compose up postgres   # the database
bun run db:migrate           # schema
bun dev                      # control plane, local Cloudflare runtime
docker compose up --build    # or: the whole control plane as containers

cargo build --workspace      # guest components
cargo test --workspace

The guest components expect a Linux host with a Wayland-capable GPU stack, and are not much use on their own yet — they are pieces of a box, and the thing that assembles a box is not open yet.

nesdoctor is the exception and needs none of that:

cargo run --release -p nesdoctor

Status

Working: nesdoctor — released, and the only part a stranger can operate today. The API, auth, the domain model, and the guest components listed above.

Not here yet: the box lifecycle, storage, the edge, and the client. Some of that will open as it is written; some is deliberately closed. What decides which is whether it handles your data — that half is open on principle — or decides our capacity, which is the part we sell.

Contributing

Early, and the ground moves. The two most useful things you can do right now cost a minute each: run nesdoctor and send the result, because we have almost no idea what the machines on the other end of this look like; and tell us where the documentation failed you. Conventional commits; explain why in the body.

Licence

Apache 2.0.

Description
[Experimental] Open-source GeForce NOW alternative with Stadia's social features
Readme 187 MiB
Languages
TypeScript 73%
Go 11.9%
Rust 9.5%
Shell 2%
CSS 1.4%
Other 2.1%