diff --git a/.env.example b/.env.example
index d1b17a10..428285e7 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,10 @@ TEST_DATABASE_URL=
DATABASE_PASSWORD=
DATABASE_URL=
DATABASE_HOST=
+DATABASE_PORT=
+DATABASE_USER=
+ADMIN_SHARED_SECRET=
+SSH_AUTH_KEY=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..871d9af3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+
+
+
+
+# Nestri
+
+Cloud game streaming platform — play your games from any device via QUIC low-latency streams.
+
+- **Streaming core** — QUIC-based relay, game machines, pairing codes
+- **Games** — Steam-linked catalog with per-machine depot downloads
+- **Auth** — Steam / SSH login via a self-hosted OpenAuth issuer
+- **Infra** — Cloudflare Workers + Postgres, deployed with [Alchemy](https://alchemy.run)
diff --git a/alchemy.run.ts b/alchemy.run.ts
index fd2455ef..d6b70a61 100644
--- a/alchemy.run.ts
+++ b/alchemy.run.ts
@@ -17,10 +17,10 @@ const Database = Effect.gen(function* () {
return yield* Cloudflare.Hyperdrive.Connection('db', {
origin: {
scheme: 'postgres',
- host: 'public-nestri-pg-1-atdogthbymao.db.upclouddatabases.com',
- port: 11569,
+ host: process.env.DATABASE_HOST ?? 'localhost',
+ port: Number(process.env.DATABASE_PORT ?? 5432),
database,
- user: 'upadmin',
+ user: process.env.DATABASE_USER ?? 'postgres',
password: Redacted.make(process.env.DATABASE_PASSWORD!)
},
dev: {
diff --git a/apps/api/README.md b/apps/api/README.md
new file mode 100644
index 00000000..230002a5
--- /dev/null
+++ b/apps/api/README.md
@@ -0,0 +1,44 @@
+# apps/api
+
+The public HTTP API for Nestri — a [Hono](https://hono.dev) app deployed as a Cloudflare Worker.
+
+## What it does
+
+Exposes the JSON API consumed by frontends and other clients. Every route is a thin wrapper that
+validates input, delegates to a domain function in [`@nestri/core`](../../packages/core/README.md),
+and returns `{ data: ... }`. All business logic lives in the core package.
+
+Routes:
+
+| Prefix | Purpose |
+| ----------------- | ------------------------------------------------------------- |
+| `/` | Health check |
+| `/user` | Current user profile, fingerprints, linked accounts |
+| `/steam` | Link / sync / unlink a Steam account |
+| `/library` | Owned games with playtime |
+| `/games` | Game catalog |
+| `/pairing-code` | Device pairing codes |
+| `/machine` | Host machines |
+| `/access-token` | Short-lived access tokens |
+| `/doc` | Generated OpenAPI spec |
+
+## Structure
+
+```text
+app/
+ index.ts # Hono entrypoint: middleware, routes, error handler, /doc
+ middleware/auth.ts # Bearer JWT + admin shared-secret auth → Actor
+ routes/*.ts # Thin route namespaces (UserApi, SteamApi, ...)
+ utils/ # ErrorResponses, Result(), validator wrapping
+test/ # Route tests (Vitest/Bun)
+```
+
+## Key details
+
+- Auth: `Authorization: Bearer ` verified against `@nestri/auth`; or the `x-nestri-admin-token` header (see `ADMIN_SHARED_SECRET`).
+- Errors: centralized `VisibleError` → typed JSON responses.
+- The API worker receives its bindings (`AUTH`, `HYPERDRIVE`, `STEAM_API_KEY`, `ADMIN_SHARED_SECRET`) from Alchemy — see `alchemy.run.ts` at the repo root.
+
+## Running
+
+Run via the root Alchemy setup (`bun alchemy.run.ts --dev`). Needs a Postgres database and an auth worker; see the root README for full dev setup.
\ No newline at end of file
diff --git a/apps/auth/README.md b/apps/auth/README.md
new file mode 100644
index 00000000..5813608c
--- /dev/null
+++ b/apps/auth/README.md
@@ -0,0 +1,32 @@
+# apps/auth
+
+The authentication worker for Nestri — a Cloudflare Worker built on
+[`@nestri/auth`](../../packages/auth/README.md) (OpenAuth-style issuer).
+
+## What it does
+
+Hosts the OpenID Connect / OAuth issuer and the login UI:
+
+- **Steam OAuth** — the primary login flow. After Steam redirects back, the worker fetches the
+ player's profile, creates (or finds) the `User` + `LinkedAccount` rows in Postgres, auto-creates a
+ personal team on first login, and issues a JWT `user` subject containing `{ userID, linkedAccountID }`.
+- **SSH login** — authenticates a device via its SSH fingerprint (keyed by `SSH_AUTH_KEY`),
+ resolving the identity through `Steam.resolveSshIdentity` in `@nestri/core`.
+
+## Key details
+
+- Signing keys are generated at runtime and persisted in the `AuthStorage` KV namespace.
+- JWT subjects are defined in `@nestri/core/auth/subjects`.
+- The API worker calls this worker via a service binding (`AUTH`), verified through `AUTH_ISSUER_URL`.
+
+## Structure
+
+```text
+src/index.ts # Worker entrypoint: issuer config + success callbacks (steam, ssh)
+test/ # Worker tests
+```
+
+## Running
+
+Deployed through Alchemy (`apps/auth` worker in `alchemy.run.ts` at the repo root) with bindings
+`AuthStorage` (KV), `HYPERDRIVE` (Postgres), `STEAM_API_KEY`, `SSH_AUTH_KEY`.
diff --git a/packages/auth/README.md b/packages/auth/README.md
new file mode 100644
index 00000000..b92cf341
--- /dev/null
+++ b/packages/auth/README.md
@@ -0,0 +1,46 @@
+# packages/auth (`@nestri/auth`)
+
+Framework-agnostic OpenAuth implementation for Nestri — the OAuth/OIDC **issuer**, **client**,
+**subjects**, and the login **UI**. A vendored/forked build of **OpenAuth**.
+
+## What it does
+
+Everything needed to run your own authentication provider:
+
+- **`issuer.ts`** — the authorization server: routes for `/authorize`, `/callback`, `/token`,
+ `/userinfo`, `.well-known/*`, plus the login UI (React renderer).
+- **`client.ts`** — `createClient` to verify JWTs against the issuer ("who is this token?").
+- **`subject.ts`** — typed JWT subjects (`zod` schemas for the token payload).
+- **`provider/*`** — drop-in OAuth/OIDC providers (steam, discord, github, google, apple,
+ microsoft, slack, spotify, twitch, x, yahoo, facebook, linkedin, cognito, keycloak, jumpcloud,
+ oauth2, oidc, password, ssh, code, arctic).
+- **`storage/*`** — persistence adapters for keys/sessions/codes: `memory`, `cloudflare` (KV),
+ `aws`, `dynamo`.
+- **`ui/*`** — the login page components (forms, password, code, theme, CSS).
+- **`jwt.ts`, `keys.ts`, `pkce.ts`, `random.ts`** — signing, keypair management, PKCE, randomness.
+
+## Usage
+
+Consumed by the [`apps/auth`](../../apps/auth/README.md) worker, e.g.:
+
+```ts
+import { issuer } from '@nestri/auth/index';
+import { CloudflareStorage } from '@nestri/auth/storage/cloudflare';
+import { SteamProvider } from '@nestri/auth/provider/steam';
+```
+
+The API uses `createClient` (from `@openauth/openauth/client`) or the bundled `client.ts` to verify
+tokens against the issuer URL.
+
+## Scripts
+
+```sh
+bun test # run tests
+bun run build # build (see script/build.ts)
+```
+
+## Note
+
+`@openauthjs` is the upstream project; this package's exports are meant to be API-compatible with a
+pinned preference toward tree-shaking-friendly imports. Prefer importing subpaths over the barrel
+(`@nestri/auth/index`).
\ No newline at end of file
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 00000000..9646c9b4
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,60 @@
+# packages/core
+
+`@nestri/core` — the **domain layer** for Nestri. All business logic, database access, and
+serialization lives here. The API and auth workers are thin pass-through translation layers on top.
+
+## What it contains
+
+| Area | Files | Purpose |
+| ---- | ----- | ------- |
+| **db** | `db/index.ts`, `db/types.ts`, `db/test.ts` | Drizzle + Postgres (`Database.use/transaction`), ULID column helpers |
+| **users** | `user/*` | Users, linked accounts, fingerprints, library |
+| **teams** | `team/*` | Teams + membership with roles (`team_member`) |
+| **games** | `game/*` | Game catalog, depot content, per-host downloads |
+| **steam** | `steam/index.ts` | Steam API integration & SSH identity resolution |
+| **auth** | `auth/subjects.ts` | JWT subjects shared with the auth worker |
+| **infra** | `env.ts`, `context.ts`, `actor.ts`, `fn.ts`, `id.ts`, `error.ts`, `examples.ts` | Environment, Actor model, zod-typed `fn()` wrappers, IDs, error types, examples |
+| **migrations** | `migrations/` | Drizzle-kit SQL migrations for Postgres schema |
+
+## Conventions
+
+- **Domain namespaces** (`user/`, `team/`, ...) expose typed `fn()` functions that validate input
+ with a Zod schema and serialize DB rows inside the function boundary — the API routes never see raw table rows.
+- **Actor model**: `Actor.userID`, `Actor.type`, ... pull the current authenticated identity from
+ `AsyncLocalStorage` (set by the API middleware / auth worker) without passing it through call chains.
+- **Soft delete**: every table has `time_deleted`; queries filter with `isNull(table.timeDeleted)`.
+- **IDs**: ULIDs via `Identifier.ascending('user')` → `usr_...`.
+- Tables are defined in `*.sql.ts` files (drizzle) with namespaces in `index.ts`.
+- Environment is read through `Env.get()`, init by worker bindings.
+
+## Structure
+
+```text
+src/
+├── actor.ts, env.ts, id.ts, fn.ts, error.ts, examples.ts
+├── db/
+├── auth/
+├── user/ (user.sql.ts, linked-account.*, fingerprint.*, library.*, index.ts)
+├── team/ (team.sql.ts, member.*, index.ts)
+├── game/ (game.sql.ts, depot.*, download.*, index.ts)
+├── steam/ (index.ts)
+├── pairing-code/
+├── access-token/
+└── machine/
+```
+
+## Scripts
+
+```sh
+bun run db:push # push schema (drizzle-kit)
+bun run db # open drizzle-kit
+```
+
+## Usage
+
+```ts
+import { Team } from '@nestri/core/team/index';
+import { Database } from '@nestri/core/db/index';
+
+const team = await Team.fromID('tem_...');
+```
\ No newline at end of file
diff --git a/wordmark.svg b/wordmark.svg
new file mode 100644
index 00000000..75c45197
--- /dev/null
+++ b/wordmark.svg
@@ -0,0 +1,43 @@
+
+
+
+