---
title: "@playstack/auth"
description: "Framework-independent identity and opaque session primitives for Node and Workers."
tags: ["package","identity","auth","sessions","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@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` property | Required | Purpose                                                             |
| --------------------------------- | -------- | ------------------------------------------------------------------- |
| `persistence`                     | Yes      | Transactional session and refresh-token state.                      |
| `crypto`                          | Yes      | Random tokens, SHA-256 digests, and replacement-token encryption.   |
| `events`                          | Yes      | Typed auth event emitter.                                           |
| `clock`, `ids`                    | Yes      | Application-owned time and ID generation.                           |
| `deviceValidator`                 | No       | Rejects inactive device-backed sessions.                            |
| `requireDevice`                   | No       | Requires a device ID on issue; defaults to `false`.                 |
| `accessTtlMs`                     | No       | Access lifetime; defaults to 15 minutes.                            |
| `refreshTtlMs`                    | No       | Refresh lifetime; defaults to 30 days.                              |
| `refreshGraceMs`                  | No       | Parallel-refresh replacement grace; defaults to 30 seconds.         |
| `reusePolicy`                     | No       | Reuse 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` property | Required | Bridge responsibility                                                         |
| ----------------------------------- | -------- | ----------------------------------------------------------------------------- |
| `persistence`                       | Yes      | Users, identities, one-time credentials, pending email changes, and sessions. |
| `crypto`                            | Yes      | Opaque token generation, hashing, and encryption.                             |
| `passwordHasher`                    | Yes      | Password-grade hash and verify operations.                                    |
| `breachChecker`                     | Yes      | Checks candidate passwords against the application’s breach source.           |
| `limiter`                           | Yes      | Fails closed for identifier-plus-IP auth attempts.                            |
| `delivery`                          | Yes      | Sends verification, reset, magic-link, and email-change notifications.        |
| `stepUp`                            | Yes      | Verifies a recent-auth or MFA proof for sensitive changes.                    |
| `events`, `clock`, `ids`            | Yes      | Typed events and deterministic primitives.                                    |
| `dummyPasswordHash`                 | Yes      | Valid hash verified for unknown users to reduce timing disclosure.            |
| `tokenTtlMs`                        | No       | One-time token lifetime, 15–60 minutes; defaults to 30 minutes.               |

## Primary bridge contracts

| Contract                 | Key members                                                                                                |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `AuthPersistence`        | Transaction, session lookup/save/revoke, refresh-family locking and rotation.                              |
| `CredentialPersistence`  | Extends auth persistence with user, identity, one-time token, and email-change records.                    |
| `AuthCrypto`             | `randomToken`, `digestSha256`, `encrypt`, `decryptText`; structurally compatible with `@playstack/crypto`. |
| `PasswordHasher`         | `hash(password)` and `verify(hash, password)`.                                                             |
| `PasswordBreachChecker`  | `isBreached(password)`.                                                                                    |
| `AuthAttemptLimiter`     | `consume(operation, { identifier, ip })`; returns `allowed` and optional `retryAt`.                        |
| `AuthDelivery`           | Token delivery plus password- and email-change notifications.                                              |
| `StepUpVerifier`         | `verify(userId, proof)`; `MfaService` implements it structurally.                                          |
| `DeviceSessionValidator` | `assertActive(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](/features/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.

{/* package-reference:start */}

## 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 point | Declaration 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.json` | No TypeScript declaration (asset or metadata export). |
| `@playstack/auth/package.json` | No 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.

| Peer | Compatible range | When needed |
| --- | --- | --- |
| `argon2` | `>=0.44.0 <1` | Optional; only for the entry points that use it. |

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

{/* package-reference:end */}
