mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
The unit is one second of a reference session — baseline size, baseline card, running alone, on hardware we own. Every factor is a multiple of that, so an allowance is measured in time and a bar prints the stored number instead of converting into it. Integers throughout. Three rolling windows, one function. A counter is stored beside the time it was last written, and a counter whose timestamp falls outside its window reads as zero — so the reset is implied by the clock and nothing has to run for a window to roll clear. No scheduled job to misfire, and no race between a reset and a write landing together. The same rule on the write side is one statement rather than a read followed by a decision. The check is pure, and it is the same arithmetic the meter draws from. The complaint about usage limits is almost never the limit, it is being surprised by one, and two implementations that agree today are how a full bar and a refusal start disagreeing. Two rules on the allowances, enforced rather than remembered: - A window's allowance must exceed the window itself. Because the windows roll, one uninterrupted session asymptotes at exactly the window length, so an allowance at or below it is a wall that someone playing alone will meet. - Each longer allowance must be under what the shorter window already permits, or it can never be reached — a number that looks like a limit, reads like a promise, and never once fires. Allowances are configuration rather than constants, because they will be retuned against real burn far more often than this code changes, and a rate that needs a deploy is a rate that stays wrong until the next one. The shipped set is explicitly a placeholder: coherent enough to test against, not a pricing decision.
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_...');