mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-20 01:35:19 +03:00
Every team now exists with the payment provider, free ones included. An upgrade then changes a subscription rather than inventing a customer, and there is one question to ask about anybody instead of two. A subscription at nothing a month needs no payment, so it is created outright rather than by sending somebody through a checkout to pay zero. It runs after the team rows are committed and cannot affect them. Signing up is not allowed to depend on a third party being reachable, so this cannot fail the call and does not retry — a team that misses it is free, which is what it would have been anyway, and the next call puts it right because the operation is idempotent. This broke the webhook mapping, which read the plan off the event type. A free subscription announces itself with the same `subscription.created` a paid one does, so every new signup would have landed on the paid allowance. The plan now comes from the product, and a product we do not sell is left alone rather than guessed at — somebody selling something else through the same account must not be able to change what a team may run by doing so. The external id stays the team. It is the billing subject, and keying on the user would collapse somebody with two teams into one customer with no way to say which subscription belonged to which.
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_...');