npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flametrench/tenancy

v0.2.1

Published

Tenancy primitives for Flametrench: organizations, memberships, invitations. Spec-conformant revoke-and-re-add lifecycle and atomic invitation acceptance.

Readme

@flametrench/tenancy

Tenancy primitives for Flametrench: organizations, memberships, and invitations. Spec-conformant revoke-and-re-add lifecycle, atomic invitation acceptance, sole-owner protection, and an mem_/tup_ duality that cannot drift.

Status: v0.2.0 (stable). Both the in-memory reference store and the production-ready PostgresTenancyStore ship in this package; the latter mirrors the in-memory semantics byte-for-byte at the SDK boundary with multi-statement atomicity for createOrg, changeRole revoke-and-re-add, acceptInvitation with pre-tuples, and transferOwnership. Per ADR 0013 the constructor accepts a pg.Pool (standalone) or a pg.PoolClient (adopter-managed transaction); when caller-owned, tx() cooperates via SAVEPOINT/RELEASE instead of opening its own BEGIN.

Install

pnpm add @flametrench/tenancy

Quick start

import { InMemoryTenancyStore } from "@flametrench/tenancy";
import { generate } from "@flametrench/ids";

const store = new InMemoryTenancyStore();

const alice = generate("usr") as `usr_${string}`;
const bob = generate("usr") as `usr_${string}`;

// Alice creates Acme Corp and becomes its owner.
const { org, ownerMembership } = await store.createOrg(alice);

// Alice invites Bob as a member.
const invitation = await store.createInvitation({
  orgId: org.id,
  identifier: "[email protected]",
  role: "member",
  invitedBy: alice,
  expiresAt: new Date(Date.now() + 7 * 24 * 3600_000),
});

// Bob accepts. Membership row is created and the authorization tuple is
// materialized atomically.
const { membership, materializedTuples } = await store.acceptInvitation({
  invId: invitation.id,
  asUsrId: bob,
});

// Bob gets promoted to admin. Revoke + re-add: the old mem is marked
// revoked with the new mem's `replaces` pointing at it.
const promoted = await store.changeRole({
  memId: membership.id,
  newRole: "admin",
});
console.log(promoted.replaces === membership.id); // true

API shape

Every backend implements the same TenancyStore interface:

interface TenancyStore {
  // Organizations
  createOrg(creator: UsrId): Promise<{ org: Organization; ownerMembership: Membership }>;
  getOrg(orgId: OrgId): Promise<Organization>;
  suspendOrg / reinstateOrg / revokeOrg(orgId: OrgId): Promise<Organization>;

  // Memberships
  addMember(input: AddMemberInput): Promise<Membership>;
  getMembership(memId: MemId): Promise<Membership>;
  listMembers(orgId: OrgId, options?): Promise<Page<Membership>>;
  changeRole(input: ChangeRoleInput): Promise<Membership>;
  suspendMembership / reinstateMembership(memId: MemId): Promise<Membership>;
  selfLeave(input: SelfLeaveInput): Promise<Membership>;
  adminRemove(input: AdminRemoveInput): Promise<Membership>;
  transferOwnership(input): Promise<{ fromMembership; toMembership }>;

  // Invitations
  createInvitation(input): Promise<Invitation>;
  getInvitation(invId): Promise<Invitation>;
  listInvitations(orgId, options?): Promise<Page<Invitation>>;
  acceptInvitation(input): Promise<AcceptInvitationResult>;
  declineInvitation(input): Promise<Invitation>;
  revokeInvitation(input): Promise<Invitation>;

  // Authorization tuple accessors (read-only)
  listTuplesForSubject(subjectType, subjectId): Promise<Tuple[]>;
  listTuplesForObject(objectType, objectId, relation?): Promise<Tuple[]>;
}

Using the Postgres-backed store

The Postgres implementation lives at a separate entry point so the base package stays Postgres-free. Install pg (peer dependency) alongside this package and import from @flametrench/tenancy/postgres:

import { Pool } from "pg";
import { PostgresTenancyStore } from "@flametrench/tenancy/postgres";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new PostgresTenancyStore(pool);

The reference schema (spec/reference/postgres.sql in flametrench/spec, also vendored at packages/tenancy/test/postgres-schema.sql in this repo for tests) must be applied to the target database before use. The schema pre-dates this package; applying it via your migration tool of choice is the recommended path.

Every operation that modifies more than one row runs inside a single BEGIN/COMMIT transaction, so the spec's atomicity guarantees are real database transactions:

  • createOrg: inserts org, mem, and tup in one transaction.
  • changeRole: revokes the old mem, inserts the new mem with replaces, deletes the old tup, inserts the new tup — in one transaction.
  • acceptInvitation: inserts mem, materializes the membership tup, expands all pre_tuples into tup rows, transitions the invitation — one transaction.
  • transferOwnership: demotes the old owner's mem, promotes the target's mem, swaps both corresponding tup rows — one transaction.

The Postgres store has no dependency on the identity layer, but its FK constraints require rows to exist in the usr table. Integration tests register test users explicitly; production deployments get this for free once @flametrench/identity lands.

Spec conformance

This package implements the tenancy layer of Flametrench v0.1. See the normative specification at spec/docs/tenancy.md and the design decisions at ADR 0002 and ADR 0003.

Conformance fixtures for tenancy are staged in spec/conformance/fixtures/tenancy/; the fixture harness lands alongside the Postgres-backed store.

Behaviors that are NOT yet spec-fixture-verified

Until the tenancy fixtures land (they require a stateful harness), the behaviors below are validated only by this package's internal unit tests. The unit tests match the spec exactly; the fixture-level verification adds cross-SDK byte-identity guarantees:

  • Atomic accept-invitation transaction (user creation if needed, mem_ insert, membership tup_ insert, pre_tuples expansion, invitation state transition — all in one logical transaction).
  • mem_ / tup_ duality under every lifecycle transition.
  • Sole-owner protection on self-leave, admin-remove, suspend, and role-change.
  • removed_by attribution (null ⇒ self-initiated, non-null ⇒ admin-initiated).
  • Ownership transfer atomicity.

Errors

Every error is an instance of TenancyError with a stable machine-readable code:

| Class | Code | When | |---|---|---| | NotFoundError | not_found | Referenced entity does not exist. | | SoleOwnerError | conflict.sole_owner | Operation would leave the org ownerless. | | RoleHierarchyError | forbidden.role_hierarchy | Admin rank insufficient for target. | | DuplicateMembershipError | conflict.duplicate_membership | User already has an active membership in this org. | | AlreadyTerminalError | conflict.already_terminal | Entity is already in a terminal state. | | InvitationExpiredError | conflict.invitation_expired | Invitation's TTL has elapsed. | | InvitationNotPendingError | conflict.invitation_not_pending | Invitation is already in a terminal state. | | ForbiddenError | forbidden | Caller is not authorized. | | PreconditionError | precondition.<specific> | A specific precondition was not met. |

Development

pnpm install
pnpm -r build       # builds @flametrench/ids first, then @flametrench/tenancy
pnpm -r test        # 60 ids tests + 43 tenancy tests
pnpm -r typecheck

License

Apache License 2.0. Copyright 2026 NDC Digital, LLC.