Files
netris-nestri/packages/core
Wanjohi 60c4f61bfa feat(billing): free is a subscription too, and the product says which plan
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.
2026-09-19 01:07:01 +03:00
..
2026-08-06 22:13:51 +03:00
2026-08-06 22:13:51 +03:00

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

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_...');