mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
`release-prod.yml` for api and auth, mirroring the edge's. The gate is not "you may not merge" — it is that merging does not deploy: tests run against a real Postgres, both binaries are executed, and a failure anywhere means no release exists, so the machine keeps serving what it has. The new piece is `nestri-migrate`, because the deploy runs migrations *before* it swaps a release into place and had nothing to run. `drizzle-kit migrate` reads the migrations folder at runtime, which is right on a laptop and wrong on a server: the deploy ships flat, checksummed files into bin/, and a migrator that needs a directory beside it can be pointed at the wrong directory. So the folder is baked into the binary — generated on every build rather than committed, so it cannot drift — and the artefact's checksum then covers every statement it will run. It reimplements drizzle's bookkeeping in thirty lines of SQL rather than calling into `db.dialect.migrate`: same table, same schema, same sha256 over the whole file, same high-water-mark comparison. That equivalence is the one thing that must not rot, because the production database was first migrated by drizzle-kit and a disagreement means a migration applied twice. Two checks hold it: CI applies the migrations with drizzle-kit and then asserts the embedded set reports nothing pending, and the hashes were verified by hand against the live database — all fourteen match to the byte. Proven before shipping, against the real database: nothing pending on the deployed schema, 14 unchanged rows, and a scratch database migrated from empty to the same 22 tables and the same high-water mark, idempotent on a second run. Refuses with exit 2 when DATABASE_URL is absent rather than defaulting to localhost, which would be a migrator reporting success having migrated nothing. setup-bun is pinned to a commit and not to `v2`. A moving major tag is fine everywhere else in this repository; this workflow is the only thing between a merge and a process serving users, and there is no first-party bun action to prefer instead.
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 typedfn()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 fromAsyncLocalStorage(set by the API middleware / auth worker) without passing it through call chains. - Soft delete: every table has
time_deleted; queries filter withisNull(table.timeDeleted). - IDs: ULIDs via
Identifier.ascending('user')→usr_.... - Tables are defined in
*.sql.tsfiles (drizzle) with namespaces inindex.ts. - Environment is read through
Env.get(), init by worker bindings.
Structure
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
bun run db:push # push schema (drizzle-kit)
bun run db # open drizzle-kit
Usage
import { Team } from '@nestri/core/team/index';
import { Database } from '@nestri/core/db/index';
const team = await Team.fromID('tem_...');