Wanjohi 8c80c025be feat(deploy): drop the IaC layer, and make both apps runnable as containers (#325)
Moving the issuer's state into Postgres (#324) removed the last thing
tying either app to one hosting provider. What was left was a deployment
tool describing resources that no longer existed — so this drops it in
favour of `wrangler`, which is what actually deploys a Worker, and adds
a second way to run each app that involves no provider at all.

## What changes

**Gone:** `alchemy.run.ts`, `docs/alchemy.md`, the `alchemy` and
`effect` root dependencies, and the two type imports that reached out of
`apps/api` into the infrastructure file.

**In its place**, per app:

| | |
|---|---|
| `wrangler.jsonc` | one environment per stage, custom-domain routes,
Hyperdrive, the `AUTH` service binding |
| `Dockerfile` + `server.ts` | the same handler behind a listening
socket |

The handler is the same one either way. What differs is only where its
settings come from, and two of them gained a second spelling so that
nothing has to branch on the runtime: Postgres arrives as a pooled
binding or as `DATABASE_URL`, and the route to the issuer is a service
binding or `AUTH_INTERNAL_URL`.

`docker-compose.yml` now brings up Postgres and both apps together,
which is both what a self-hoster runs and the shape this takes when it
stops being a set of Workers.

## `AUTH_INTERNAL_URL`, which is new

A service binding was quietly doing two jobs: routing to the issuer, and
letting the `iss` claim stay the issuer's public name. Nothing else can
do both with one setting — the public name is often not routable from
inside a deployment — so the name and the route are two settings now.
`AUTH_ISSUER_URL` is still compared literally against every token, and
is unchanged.

## DNS

Moves out of code and into [`docs/dns.md`](docs/dns.md): every hostname,
what it is for, and what answers it today. Six records that change
roughly never did not need a tool, and a table outlives whatever is
serving the names — which is the point, because some of them will stop
being Workers. `wrangler` keeps owning only the part that must stay in
step with a deploy, since a route and its hostname are one fact.

The one rule the table enforces is **one label deep on `nestri.io`**. A
certificate for `*.nestri.io` covers one level and not two, so the
sandbox names are hyphenated rather than nested —
`api-sandbox.nestri.io` can become an ordinary proxied origin later
without a certificate having to be ordered for it first. Production
hostnames are unchanged.

## Also

`EMAIL_DEV_LOG` moves from committed configuration into
`apps/auth/.dev.vars`, which `wrangler deploy` cannot upload. Printing a
live sign-in code to a log should not be one forgotten override away
from a stage somebody else can reach.

## Before this deploys

1. The Hyperdrive ids in both `wrangler.jsonc` files are placeholders.
`wrangler hyperdrive list` has the real ones. Local development works
without them.
2. The Worker names change, so the first deploy creates new Workers.
Confirm the custom domains answer, *then* remove the old Workers and
routes — that order, or the names resolve to nothing in between.
3. Set the secrets listed in [`docs/deploy.md`](docs/deploy.md). The
issuer refuses to sign anyone in without its three mail settings.

## Checks

- 316 tests pass, 0 fail, against a freshly migrated database.
- All four wrangler environments bundle with no warnings.
- Both images build, run, and report `healthy`; the API container
reaches the issuer container and rejects a bad token as 401 rather than
500.
- `AUTH_INTERNAL_URL` verified end to end: discovery and JWKS resolve
through the internal route while `iss` stays the public name.
- `oxlint` clean apart from one pre-existing unused import in
`packages/auth`.





<!-- greptile_comment -->

<h3>Greptile Summary</h3>

This PR replaces the Alchemy deployment layer with direct Wrangler
configuration and container-based execution for both control-plane
applications.
- Adds Bun HTTP servers, production Dockerfiles, health checks, and a
Compose deployment for Postgres, auth, and API.
- Supports database and issuer routing through either Cloudflare
bindings or ordinary environment variables.
- Adds stage-specific Worker routes, service bindings, Hyperdrive
configuration, and deployment documentation.
- Moves DNS ownership and deployment guidance into dedicated
documentation.
- Removes unused Alchemy, Effect, and Steam API-key configuration.

<h3>Confidence Score: 5/5</h3>

The current changes appear safe to merge, with no established new
defects or outstanding previous findings.

The container and Worker configurations are internally consistent,
required privileged credentials no longer have Compose defaults, and
plaintext service ports are loopback-bound. The three previous threads
were manually resolved without explanation and therefore are not
outstanding.

<h3>Important Files Changed</h3>




| Filename | Overview |
|----------|----------|
| docker-compose.yml | Defines the self-hosted stack with required
credentials, loopback-bound ports, mail pass-through, health
dependencies, and internal issuer routing. |
| apps/api/app/middleware/auth.ts | Adds issuer access through either a
Worker service binding or AUTH_INTERNAL_URL while preserving the public
issuer used for token validation. |
| apps/api/app/server.ts | Exposes the existing API handler through
Bun’s HTTP server with a process-compatible execution context. |
| apps/auth/src/server.ts | Exposes the existing authentication handler
through Bun’s HTTP server. |
| apps/api/wrangler.jsonc | Configures API development, sandbox, and
production Workers with routes, Hyperdrive, issuer settings, and auth
service bindings. |
| apps/auth/wrangler.jsonc | Configures auth development, sandbox, and
production Workers with custom domains and Hyperdrive. |
| packages/core/src/env.ts | Supports database and issuer routing
through environment variables and removes an unused Steam API-key
setting. |


<h3>Flowchart</h3>

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Client[Clients] --> Proxy[Custom domain or TLS proxy]
    Proxy --> API[API handler]
    Proxy --> Auth[Auth issuer handler]
    API -->|AUTH service binding| Auth
    API -->|AUTH_INTERNAL_URL in containers| Auth
    API --> DB[(Postgres)]
    Auth --> DB

    Wrangler[Cloudflare Wrangler runtime] --> API
    Wrangler --> Auth
    Compose[Docker Compose runtime] --> API
    Compose --> Auth
    Compose --> DB
```

<sub>Reviews (2): Last reviewed commit: ["fix(deploy): require every
credential,
a..."](f30a1432f8)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=60746897)</sub>

<!-- /greptile_comment -->
2026-09-05 13:19:44 +00: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 154 MiB
Languages
TypeScript 73%
Go 11.9%
Rust 9.5%
Shell 2%
CSS 1.4%
Other 2.1%