On this page
  1. Install
  2. Compose sessions and credentials
  3. Session configuration
  4. Session security and maintenance
  5. Credential configuration
  6. Primary bridge contracts
  7. Website sign-in with IndieAuth
  8. Realtime composition
  9. Web and persistence adapters
  10. Migrate an established application
  11. Boundary
  12. Explicit identity and replay policies
  13. Optional session lifetime and admission policies
  14. API entry points and requirements
  15. Peer dependencies

@playstack/auth

Framework-independent identity and opaque session primitives for Node and Workers.

Free. MIT licensed. Check preview availability before installing. See package access.

@playstack/auth owns framework-independent identities, credentials, and opaque session behavior for Node and Workers. It deliberately does not own accounts, authorization, device identity, or MFA.

Install

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

Install the optional argon2 peer only in the Node process that hashes passwords.

Compose sessions and credentials

ts
import { createAuthAttemptLimiter, createCredentialService, databaseSessions } from '@playstack/auth'
import { argon2id } from '@playstack/auth/node'

const sessions = databaseSessions({
  persistence,
  crypto: applicationCrypto,
  events,
  clock,
  ids,
  deviceValidator: devices,
  requireDevice: true,
  reusePolicy: 'all-user-sessions',
})

const credentials = createCredentialService({
  persistence,
  crypto: applicationCrypto,
  passwordHasher: argon2id(),
  breachChecker,
  limiter: createAuthAttemptLimiter(rateLimiter),
  delivery,
  stepUp: mfa,
  events,
  clock,
  ids,
  dummyPasswordHash: process.env.AUTH_DUMMY_PASSWORD_HASH!,
})

The same persistence instance may implement both the session and credential contracts. Transactional event handlers must use the same transactionKind so state and Tier 1 events commit atomically.

Session configuration

DatabaseSessionOptions propertyRequiredPurpose
persistenceYesTransactional session and refresh-token state.
cryptoYesRandom tokens, SHA-256 digests, and replacement-token encryption.
eventsYesTyped auth event emitter.
clock, idsYesApplication-owned time and ID generation.
deviceValidatorNoRejects inactive device-backed sessions.
requireDeviceNoRequires a device ID on issue; defaults to false.
accessTtlMsNoAccess lifetime; defaults to 15 minutes.
refreshTtlMsNoRefresh lifetime; defaults to 30 days.
refreshGraceMsNoParallel-refresh replacement grace; defaults to 30 seconds.
reusePolicyNoReuse response: session-family by default or all-user-sessions.

Refresh tokens rotate under a persistence lock. Reuse after the grace window revokes the token family and session.

Session security and maintenance

ts
const active = await sessions.listSessionsForUser(userId)
await sessions.revokeSessionForUser(userId, active[0].id)

const maintenance = createAuthMaintenance({
  persistence,
  clock,
  consumedRetentionMs: 7 * 24 * 60 * 60_000,
})

await maintenance.purgeExpired({ limit: 250 })

Session activity exposes IDs, creation and last-use timestamps, expiry, account/client/device references, and bounded IP and user-agent metadata. It never exposes access or refresh credentials. Maintenance is bounded and scheduler-neutral; one call removes at most 1,000 expired or retained consumed records across sessions, refresh records, one-time tokens, authorization codes, email changes, and staff sessions.

Credential configuration

CredentialServiceOptions propertyRequiredBridge responsibility
persistenceYesUsers, identities, one-time credentials, pending email changes, and sessions.
cryptoYesOpaque token generation, hashing, and encryption.
passwordHasherYesPassword-grade hash and verify operations.
breachCheckerYesChecks candidate passwords against the application’s breach source.
limiterYesFails closed for identifier-plus-IP auth attempts.
deliveryYesSends verification, reset, magic-link, and email-change notifications.
stepUpYesVerifies a recent-auth or MFA proof for sensitive changes.
events, clock, idsYesTyped events and deterministic primitives.
dummyPasswordHashYesValid hash verified for unknown users to reduce timing disclosure.
tokenTtlMsNoOne-time token lifetime, 15–60 minutes; defaults to 30 minutes.

Primary bridge contracts

ContractKey members
AuthPersistenceTransaction, session lookup/save/revoke, refresh-family locking and rotation.
CredentialPersistenceExtends auth persistence with user, identity, one-time token, and email-change records.
AuthCryptorandomToken, digestSha256, encrypt, decryptText; structurally compatible with @playstack/crypto.
PasswordHasherhash(password) and verify(hash, password).
PasswordBreachCheckerisBreached(password).
AuthAttemptLimiterconsume(operation, { identifier, ip }); returns allowed and optional retryAt.
AuthDeliveryToken delivery plus password- and email-change notifications.
StepUpVerifierverify(userId, proof); MfaService implements it structurally.
DeviceSessionValidatorassertActive(userId, deviceId).

CredentialService.authenticateProfile(profile, context?) is the entry point for a provider profile already normalized by Auth.js or another framework. authenticateProvider(provider, code, context?) performs the provider exchange and delegates to the same method, so linking and verified-email policy cannot drift between paths.

The transactional auth.user.registered event lets @playstack/accounts provision an initial account and owner membership in the same database unit of work. Delivery and initial session issuance remain post-commit application orchestration.

registerPassword() also accepts an explicit transaction context. Use createUnitOfWork() from @playstack/events when auth, accounts, profiles, and audit records must share one application-owned database transaction. Persistence adapters must declare the same transaction kind and physical handle; mismatches fail closed.

Website sign-in with IndieAuth

Use the optional server-side IndieAuth client when visitors should sign in with their own website URL. It verifies the website's authorization server and hands the confirmed URL to the existing credential/session system; it does not host an IndieAuth authorization server or request Micropub access tokens.

ts
import {
  createIndieAuthClient,
  createIndieAuthBrowserBinding,
  createMemoryIndieAuthAttemptStore,
} from '@playstack/auth/indieauth'
import { createNodeIndieAuthTransport } from '@playstack/auth/indieauth/node'

const indieauth = createIndieAuthClient({
  clientId: 'https://your-site.example/indieauth-client',
  redirectUri: 'https://your-site.example/auth/indieauth/callback',
  attempts: createMemoryIndieAuthAttemptStore(), // single process only
  transport: createNodeIndieAuthTransport(),
})

// After rate limiting and CSRF validation on your sign-in POST:
const browserBinding = createIndieAuthBrowserBinding()
const { authorizationUrl } = await indieauth.begin({ me: websiteUrl, browserBinding })
// Set a Secure/HttpOnly/SameSite=Lax/Path=/ host-only binding cookie;
// then redirect the browser to authorizationUrl.

// In the configured callback, read browserBinding from that cookie:
const profile = await indieauth.complete({ callbackUrl, browserBinding })
const user = await credentials.authenticateProfile(profile)
// Continue required MFA/account checks and your existing session flow.

Serve indieauth.clientMetadata as JSON at clientId. Configure the credential service with providerIdentityPolicy: { linking: 'explicit', email: 'optional' }. Only the confirmed website URL identifies the visitor. Provider email, profile attributes and tokens are discarded; first-contact email enrollment and identity linking require separate verified flows. An existing account is never selected merely because a provider claims its email address.

Attempts use S256 PKCE, browser binding, expiry and atomic single-use consumption. Multi-process/serverless deployments must supply a shared IndieAuthAttemptStore with atomic binding/expiry checks and consumption; a replayable cookie alone or get-then-delete storage is insufficient. Missing/duplicate binding cookies must fail. Keep login/callback responses private and uncached, suppress referrers, and redact callback query strings and credentials from logs. Uncertain exchanges require fresh sign-in, never an automatic code retry.

The supported profile uses public HTTPS websites and current server-metadata discovery with S256 and iss. HTTP/private/local endpoints and endpoint-only discovery are not supported. The Node transport pins public DNS answers and bounds redirects, headers, bodies and deadlines. Workers require an explicit equivalent safe transport; ordinary global fetch does not establish SSRF safety.

The installed package's README includes the full setup, storage contract, limits and error policy. Qualify your provider, browser cookies, proxy configuration, shared storage and session/MFA flow before launch. The adapter itself adds no managed schema or application routes.

Realtime composition

Issue a purpose-bound ticket from authenticated HTTP, then redeem it in the WebSocket hello frame:

ts
import { createRealtimeTicketService } from '@playstack/auth'

const tickets = createRealtimeTicketService({ sessions, crypto: applicationCrypto })
const ticket = await tickets.issue(accessToken)
const session = await tickets.redeem(ticket)

Tickets default to 30 seconds and may be configured from 1–60 seconds. Single-use safety depends on the atomic replay store configured in @playstack/crypto. Redemption re-resolves the session, so a newly revoked session cannot open a connection.

Web and persistence adapters

  • sessionCookie, clearSessionCookie, and assertCsrf enforce the v1 cookie/CSRF transport invariants without choosing a framework.
  • @playstack/auth-contracts publishes the JSON-safe session contract independently so a React or Next client can migrate before the server implementation.
  • parseAuthSessionView validates the narrow JSON-safe client projection and rejects unknown or credential-shaped fields.
  • @playstack/auth/prisma provides prismaAuthPersistence(prisma) and the managed, non-ejectable Prisma fragment.
  • @playstack/auth/testing provides in-memory persistence and crypto fakes.

Applications can register a validated extensions envelope with createAuthSessionViewParser() and createAuthSessionProjector(). The same parser must be used by server projection, React transport, account client, and Next helpers so product-specific account summaries or capabilities remain typed end to end. Tokens and provider credentials never belong in this envelope.

Migrate an established application

createMigratingPasswordHasher() verifies an explicitly configured legacy hash such as bcrypt, then rehashes with the current Argon2id policy under a compare-and-swap write. createLegacySessionExchange() provides an absolute, bounded window for exchanging an existing JWT or refresh session into ordinary Playstack opaque credentials.

Existing schemas may satisfy managed artifacts through exact model and field mappings. When physical storage differs, omit the artifact and implement application-owned AuthPersistence or CredentialPersistence; that adapter owns identifier conversion, dual reads, shadow writes, and record migration. The CLI can scaffold typed method stubs and contract-test entry points without declaring a semantically similar schema compatible.

Register sessions.deviceRevocationHandler() for devices.device.revoked and sessions.mfaChangeHandler() for MFA enrollment and disable events when those packages share the same transaction kind.

Boundary

Auth does not load secrets, choose HTTP routes, own account membership or authorization, identify devices, or decide MFA policy. Add React, Next.js, NestJS, accounts, devices, and MFA packages only at application edges that need them.

Explicit identity and replay policies

providerIdentityPolicy: { linking: 'explicit', email: 'optional' } supports email-less provider users without silently linking a matching email. Trusted server normalization and step-up-backed identity linking remain required. The compatible default is verified-email linking with required email. Safe user/session projections and registration-event email may be null; first-contact email enrollment remains an application-owned verified workflow.

New provider users emit the same transactional registration event as password signup. Required account/audit provisioning must join the actual transaction, not a post-login callback.

refreshReplayPolicy: 'strict' rejects every consumed-token replay without returning replacement material. The default bounded-grace policy retains encrypted replacement credentials for a bounded interval. reusePolicy: 'all-user-sessions' separately widens revocation scope. Strict mode rejects refreshGraceMs and requires shared client refresh coordination.

Authorization-code consumption and session issuance can commit together through required transaction-aware issuance/authorization seams with account context. A committed code cannot be replayed to recover lost credentials. Optional device authorization follows the same one-time boundary. See Client authentication.

Optional session lifetime and admission policies

databaseSessions() now accepts idleTtlMs, absoluteTtlMs and maxActiveSessions; omitted options preserve existing defaults.

  • Idle expiry is inclusive and measured from successful verify, resolve or refresh. Background requests count as activity and verification adds locked activity writes; this is not automatically human-idle detection.
  • Absolute lifetime starts at original issuance, survives refresh/account switching and clips token expiry. It cannot exceed refreshTtlMs.
  • Per-user caps reject new issuance with playstack/auth/session_limit_exceeded, never silently evict devices. Refreshable sessions count even after access expiry; caps require an explicit absolute lifetime.

Prisma supplies transaction-scoped creation locks and locked reads. Application persistence must implement lockSessionCreationForUser for caps and findSessionByIdForUpdate for idle expiry, with real cross-instance correctness. Missing seams fail configuration; no-op locks are not sufficient. All issuers must use the same policy/lock contract.

Map authenticated cap failures to a deliberate host response while retaining enumeration-safe unauthenticated errors. Strict/bounded-grace refresh replay remains independent; cookie transport, SSO, native coordination and deployment policy changes require application acceptance tests.

API entry points and requirements

Reference snapshot: @playstack/auth@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/auth./dist/index.d.ts
@playstack/auth/errors./dist/errors.d.ts
@playstack/auth/indieauth./dist/indieauth.d.ts
@playstack/auth/indieauth/node./dist/indieauth-node.d.ts
@playstack/auth/authorization./dist/authorization.d.ts
@playstack/auth/device./dist/device-authorization.d.ts
@playstack/auth/device-prisma./dist/device-prisma.d.ts
@playstack/auth/device-testing./dist/device-testing.d.ts
@playstack/auth/passwords./dist/passwords.d.ts
@playstack/auth/node./dist/node.d.ts
@playstack/auth/prisma./dist/prisma.d.ts
@playstack/auth/testing./dist/testing.d.ts
@playstack/auth/playstack.artifacts.jsonNo TypeScript declaration (asset or metadata export).
@playstack/auth/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

Keep existing framework versions that satisfy these ranges. Install optional peers only when using the corresponding adapter. The package manager resolves ordinary dependencies separately.

PeerCompatible rangeWhen needed
argon2>=0.44.0 <1Optional; only for the entry points that use it.

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