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

@digimuza/saas-core

v0.0.2

Published

Reusable, strongly typed SaaS foundation: auth, workspaces, authorization, billing, events, realtime and jobs.

Readme

@digimuza/saas-core

A reusable, strongly typed SaaS foundation. One npm package, subpath exports, PostgreSQL + Drizzle.

It answers the questions every SaaS application has to answer — who is this user, what workspace are they in, what can they do, what have they paid for, what is happening, and how do we run the slow parts — so that individual products only contain product logic.

Architecture rules and the full roadmap: CLAUDE.md.

Two layers of documentation, neither repeating the other:

  • src/README.md — a short README beside each module's code: what it is for, what you actually do with it, the gotchas. Start here.
  • docs/ — the reference: every exported function, its signature, and why it behaves the way it does.

Status: Phases 1–12 complete.


Bootstrap

npm install @digimuza/saas-core
npx sc init
# Project name (my-app):
npx sc start

sc init creates a project-owned docker-compose.yml containing PostgreSQL. sc start launches it and waits until the database is ready.

See docs/cli.md.

Install

npm install @digimuza/saas-core drizzle-orm zod
npm install @trigger.dev/sdk        # only if you use background jobs
npm install rxjs                    # only if you use reactive streams

drizzle-orm, zod, @trigger.dev/sdk and rxjs are peer dependencies. Your application references exported table objects, defines Trigger.dev tasks against the same SDK instance the core triggers through, and pipes the core's Observables with its own operators; two copies of any of them would not interoperate.

Configuration comes from Infisical, not .env files:

infisical run -- npm run dev

Modules

| Module | Import from | Guide | | --- | --- | --- | | Configuration | /config, /config/client | docs/config.md | | Database & schema | /db, /db/schema | docs/db.md | | Authentication | /auth | docs/auth.md | | Users & profiles | /users | docs/users.md | | Workspaces & members | /workspaces | docs/workspaces.md | | Roles & permissions | /authorization | docs/authorization.md | | Billing | /billing | docs/billing.md | | Entitlements | /entitlements | docs/entitlements.md | | Events | /events | docs/events.md | | Background jobs | /jobs | docs/jobs.md | | Realtime | /realtime | docs/realtime.md | | Streams (RxJS) | /streams, /streams/client | docs/streams.md | | Email | /email | docs/email.md | | Notifications | /notifications | docs/notifications.md | | Audit | /audit | docs/audit.md | | API keys | /api-keys | docs/api-keys.md | | Webhooks | /webhooks | docs/webhooks.md | | Feature flags | /feature-flags | docs/feature-flags.md | | Storage | /storage | docs/storage.md | | Files | /files | docs/files.md | | Logger | /logger | docs/logger.md | | CLI | saas-core binary | docs/cli.md | | Publishing | release workflow | docs/publishing.md | | Errors | root or /errors | docs/errors.md |

Every phase in CLAUDE.md now has an implementation. Observability beyond the logger — tracing, metrics — is the remaining gap.


The shape of a request

import { auth } from "@/lib/auth";
import { workspaces } from "@digimuza/saas-core/workspaces";
import { authorization } from "@digimuza/saas-core/authorization";
import { entitlements } from "@digimuza/saas-core/entitlements";
import { jobs } from "@digimuza/saas-core/jobs";

// 1. Who is this?
const user = await auth.requireUser();

// 2. Which workspace — verified, not taken on trust from the URL.
const context = await workspaces.requireContext({
  userId: user.id,
  workspaceId: params.workspaceId,
});

// 3. May they? 4. Has the workspace paid for it?
await authorization.require(context, "projects.create");
await entitlements.require(context, "ai_generation");

// 5. Do the slow part durably.
await jobs.trigger("generate-project", { name }, {
  workspaceId: context.workspaceId,
  actorId: user.id,
});

Steps 3 and 4 are separate on purpose. Authorization asks whether the user is permitted; entitlements ask whether the workspace has paid. A client renders an access error for one and an upsell for the other.


Design principles you will notice in the code

Tenant isolation is enforced by PostgreSQL, not by application code. workspace_member_roles stores workspace_id beside user_id and role_id so two composite foreign keys share it — granting a member of workspace A a role from workspace B is rejected by the database, and switching workspace_id to satisfy one key breaks the other. Application code that forgets a check cannot bypass it.

A workspaceId from a request cannot reach a query. Tenant-scoped operations take a WorkspaceContext, which only a verified membership lookup produces.

Non-members get 404, not 403. A 403 confirms a workspace exists and enables tenant enumeration.

Provider details stay behind adapters. WorkOS is confined to /auth, Revolut to one file in /billing, Trigger.dev to /jobs. Tests enforce each boundary, so application code never imports a vendor SDK — and swapping one means writing an adapter, not editing services.

Only the config module reads process.env. Enforced by a test across the whole package.

Idempotency and state changes are one transaction. Webhook ingestion claims an event with a unique-constraint insert rather than a check-then-act; domain events are written in the same transaction as the change they describe and dispatched only after it commits.

Limits fail closed. An entitlement the plan does not mention is zero, never unlimited.

Realtime channels are verified server-side, and there is no wildcard. A client names the channels it wants, so every name is parsed and then checked against membership. Browsers never touch LISTEN/NOTIFY — the server mediates.

The logger redacts by key and by value. Credentials never reach a log line, whether they arrive under a suspicious field name or as a suspicious-looking value under an innocent one.

Object keys are validated before they are prefixed, not after. A ../ in a storage key is a cross-tenant read with no database involved; the scoped API takes paths rather than keys so the tenant prefix cannot be bypassed.

Secrets are hashed and revealed once. API keys and invitation tokens store only a digest; a database leak does not let an attacker use them. Comparisons are constant-time.

The audit trail is immutable in the database, not by convention. A trigger rejects UPDATE and DELETE on audit_logs, so a migration or a stray psql session cannot quietly rewrite history.


Development

npm run typecheck
npm test
npm run build
npm run db:generate                    # after editing src/db/schema
infisical run -- npm run db:migrate

Tests run against real PostgreSQL — @electric-sql/pglite, a devDependency — with the real migrations applied. The guarantees under test are database constraints (composite foreign keys, partial unique indexes, cascades), and a mock would happily accept the cross-tenant writes the schema exists to reject.


Known gaps

  • No live-service verification. PGlite is real PostgreSQL, so the schema and constraints are genuinely exercised — but postgres.js against a server has never been run, and neither has any Revolut HTTP call or Trigger.dev enqueue.
  • The publish workflow has never run on GitHub Actions. Every gate was executed locally, the YAML parses, and npm publish --dry-run succeeds — but no Actions run and no real publish has happened, and NPM_TOKEN is not yet set.
  • The CLI has never started a real container. Template generation, Docker preflight, compose argument construction and every error path are tested with an injected runner, and the generated compose file passes docker compose config — but no Docker daemon was available to bring the stack up end to end.
  • Revolut's createCustomer and createCheckout are unverified against live credentials. Signature verification is implemented and unit-tested; the HTTP calls are written to the documented endpoints and nothing more.
  • Revolut has no hosted portal and no server-side subscription object, so getPortal() returns { url: undefined } and cancellation is recorded locally. See docs/billing.md.
  • Sending email is not transactional with the database. A retried request can send twice; idempotencyKey is the only defence, and a provider that ignores it will deliver twice regardless.
  • Resend's HTTP call is unverified against live credentials, like Revolut's.
  • Outgoing webhook URL checking is a first pass, not a complete SSRF defence. Loopback, RFC1918 and link-local hosts are refused, but DNS can still resolve a public name to a private address at connect time; production should also route egress through a proxy. See docs/webhooks.md.
  • entitlements.consume() is not transactional across its check and insert, so concurrent callers can overshoot a quota by their in-flight quantity. Deliberate; see docs/entitlements.md.
  • Server-only enforcement is a runtime guard, not build-time. The server-only package cannot be used here because it breaks plain-Node consumers; see docs/config.md.
  • The default realtime transport fans out to every server (one PostgreSQL notification channel, filtered per subscriber), and SSE holds a long-lived request that serverless platforms will cut at their duration cap. Both are documented trade-offs with a transport seam to replace; see docs/realtime.md.