On this page
  1. Install
  2. Compose the account service
  3. Configuration reference
  4. Bridge contracts
  5. Management and ownership lifecycle
  6. Optional multiple owners
  7. Resolve within a protected write
  8. Registration and recoverable deletion
  9. Fail-closed Prisma scoping
  10. Persistence and testing
  11. Boundary
  12. Restrict service policy without replacing the core
  13. API entry points and requirements
  14. Peer dependencies

@playstack/accounts

Framework-independent tenancy, membership, ownership, invitations, and fail-closed account scoping.

Pro. Covered by the Playstack Pro License. Registry access is required; check preview availability before installing. See package access.

@playstack/accounts models the tenant a user is acting within, the membership that permits that scope, and the invitation and ownership lifecycle around it.

Install

sh
npm install @playstack/accounts @playstack/auth @playstack/core @playstack/events

Compose the account service

ts
import { createAccountsService } from '@playstack/accounts'

export const accounts = createAccountsService({
  persistence,
  users: {
    assertActive: (userId) => userDirectory.assertActive(userId),
    emailForUser: (userId) => userDirectory.emailForUser(userId),
  },
  crypto: applicationCrypto,
  delivery: accountInviteDelivery,
  ownershipTransferDelivery,
  events,
  clock,
  ids,
  invitationTtlMs: 24 * 60 * 60_000,
  ownershipTransferTtlMs: 24 * 60 * 60_000,
  deletionGraceMs: 30 * 24 * 60 * 60_000,
  deletionCascades: [removeProductWorkspace],
})

const resolution = await accounts.resolve(session.userId, session.accountId)

Configuration reference

AccountsServiceOptions propertyRequiredPurpose
persistenceYesTransactional account, member, and invitation storage.
usersYesValidates active users and resolves invitation email addresses.
cryptoYesGenerates and hashes opaque invitation tokens.
deliveryYesDelivers the one-time invitation secret.
ownershipTransferDeliveryYesDelivers the separately scoped ownership-transfer bearer.
eventsYesEmits typed membership and account lifecycle events.
clock, idsYesApplication-owned time and identifiers.
invitationTtlMsNoPositive invitation lifetime; defaults to 30 minutes.
ownershipTransferTtlMsNoPositive proposal lifetime; defaults to 24 hours.
deletionGraceMsNoRecoverable deletion window; defaults to 30 days.
deletionCascadesNoUniquely named same-database purge hooks.
ownershipNosingle by default; multi permits multiple owner memberships.
administrationNoowners-and-admins by default; owners-only restricts administration.
mutationPolicyNoTransaction-aware restrictions for product-owned lifecycle rules.

AccountRole includes owner, admin, and member while allowing application-defined role strings. Domain authorization remains application policy.

Bridge contracts

ContractKey members
AccountsPersistenceTransaction, account lookup/save, member lookup/list/save/delete, and replay-safe invitation lookup/save.
AccountsUserDirectoryassertActive(userId) and emailForUser(userId).
AccountsCryptorandomToken and digestSha256; compatible with @playstack/crypto.
AccountInviteDeliverydeliver({ invitationId, accountId, email, role, token, expiresAt }).
AccountSessionStrategyswitchAccount(token, accountId, context?); implemented by database auth sessions.

An account switch validates current membership, creates a new account-scoped session, and revokes the old session through the supplied strategy. Register auth’s account-membership-removal handler for accounts.member.removed when both persistence adapters share a transaction kind.

Management and ownership lifecycle

ts
const members = await accounts.listMembers(accountId, actingUserId)
const invitations = await accounts.listInvitations(accountId, actingUserId)

await accounts.updateAccount(accountId, { name: 'Acme Studio' }, actingUserId)
await accounts.leaveAccount(accountId, actingUserId)

const proposal = await accounts.requestOwnershipTransfer(
  accountId,
  nextOwnerId,
  currentOwnerId,
)
await accounts.acceptOwnershipTransfer(deliveredToken, nextOwnerId)

The target must already be a member. A proposal is single-use, owner-only to request or cancel, and expires after 24 hours by default. Acceptance updates the owner pointer and both membership roles atomically. The current owner cannot leave or be removed through generic membership operations in the default single-owner mode.

Optional multiple owners

Set ownership: 'multi' to give every owner membership equal owner authority. Account.ownerId remains a primary-owner pointer, not the only administrator. Account-locked role changes/removal/leave protect the last owner; if the primary leaves, the remaining owner with the lexicographically smallest user ID becomes primary in the same transaction. Required ownership and membership events must commit with the change.

Owners can invite/promote owners; admins cannot grant or revoke ownership. A transfer moves the requesting owner's role to a non-owner member, preserving other owners. Personal-account membership restrictions remain application policy, checked on direct mutations and invitation acceptance. Conversion to a team must be explicit and audited, not inferred from an invitation.

Resolve within a protected write

resolve(userId, accountId, { transaction, lock: 'update' }) joins the caller's transaction and requests an authorization lock. Resolution invokes the configured users.assertActive with the transaction as well as checking account and membership state. Pass the actual shared handle through user-directory and persistence ports; do not open independent transactions for protected writes. Without options, resolution retains its own transaction boundary.

Registration and recoverable deletion

Register accounts.userRegistrationHandler() on the transactional auth.user.registered event when auth and accounts share a transaction kind. The handler creates the account and owner membership before the new auth user commits; returning null deliberately skips provisioning.

requestAccountDeletion() hides an account immediately and returns its purge deadline. cancelAccountDeletion() restores it during the grace period. purgeDeletedAccounts({ limit }) locks due accounts, invokes the configured same-database cascades on the shared transaction, and removes package-owned rows. Billing cancellation and cross-database erasure consume the lifecycle events as application workflows; Playstack never starts a scheduler.

Fail-closed Prisma scoping

ts
import { withAccountScope } from '@playstack/accounts'

const db = withAccountScope(rootPrisma, resolution.accountId, {
  tenantModels: ['project', 'document'],
  nonTenantModels: ['country'],
})

const projects = await db.project.findMany({ where: { archived: false } })
AccountScopeOptions propertyPurpose
tenantModelsAllowlist of models that receive enforced accountId constraints.
nonTenantModelsAllowlist of explicitly safe global models.

The scoped client denies unknown models, raw SQL, root transactions, unsupported operations, account-filter overrides, mismatched compound keys, and nested writes. Cross-tenant jobs must use the explicitly named root client instead of weakening the scoped client.

Persistence and testing

@playstack/accounts/prisma exports prismaAccountsPersistence(prisma). It serializes account mutations, uses advisory locks for slug allocation, locks invitation rows during acceptance, and normalizes dates. The managed schema fragment remains application-migrated. In-memory adapters are available from @playstack/accounts/testing.

Boundary

Accounts depends on authentication but does not replace it. It does not own arbitrary resource authorization, billing policy, or cross-tenant administration. Accounts/team management and its bindings remain paid as an explicit policy exception; reconsideration for Free requires a later adoption decision. Authentication does not require Accounts.

Restrict service policy without replacing the core

administration: 'owners-only' restricts membership administration; the default remains owners-and-admins. mutationPolicy.assertAllowed(input, transaction) can enforce product-owned personal-account rules before writes. Hooks restrict built-in permissions rather than grant extra authority, and must use the supplied transaction.

slugPolicy supports normalization and reserved-slug decisions. createAccountPolicy() provides named permissions for the Nest guard; service policy and resource authorization remain separate. Single-owner remains the default; choose ownership: 'multi' explicitly rather than trying to grant ownership through a restrictive hook. Personal-to-team conversion is never automatic.

An optional Nest resolveRequestAccount strategy supports user-wide sessions with current membership checks. A session already scoped to an account cannot select another; ordinary account switching retains its session-rotation behavior.

API entry points and requirements

Reference snapshot: @playstack/accounts@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

Public entry pointDeclaration file
@playstack/accounts./dist/index.d.ts
@playstack/accounts/errors./dist/errors.d.ts
@playstack/accounts/prisma./dist/prisma.d.ts
@playstack/accounts/testing./dist/testing.d.ts
@playstack/accounts/playstack.artifacts.jsonNo TypeScript declaration (asset or metadata export).
@playstack/accounts/package.jsonNo TypeScript declaration (asset or metadata export).

Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.

Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

Go

Playstack Pro tag
OriginsPricingBlogNewsletterChangelogStatusRoadmap
ContributorsCommunityIn Use ShowcaseCase StudiesPartnersSponsors
FAQsSupportContact

© 2026 Playstack. All rights reserved.

With OSS
Terms of ServicePrivacy PolicyCookie PolicyImprint

By

Commune Software