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

@saasifier/database

v0.1.1

Published

The `DatabaseAdapter` interface SaaSify's core runtime is built against, plus an in-memory reference implementation and a shared contract test suite.

Readme

@saasifier/database

The DatabaseAdapter interface SaaSify's core runtime is built against, plus an in-memory reference implementation and a shared contract test suite.

Installation

npm install @saasifier/database

Overview

SaaSify never talks to a database directly — @saasifier/core operates entirely against the DatabaseAdapter interface defined here, so any storage backend (Postgres, MongoDB, a custom ORM) can be plugged in by implementing it. This package ships that interface, createMemoryAdapter() (a zero-dependency, non-persistent implementation used in tests and playgrounds), and runDatabaseAdapterContractTests(), a reusable Vitest suite that every real adapter (e.g. @saasifier/database-postgres) is expected to pass so it can't silently drift from the interface's guarantees — in particular, tenant scoping (every method takes an explicit organizationId) and atomic usage-limit increments.

Usage

import { createMemoryAdapter, type DatabaseAdapter } from "@saasifier/database";
import { createSaaS } from "@saasifier/core";

const database: DatabaseAdapter = createMemoryAdapter();

const saas = createSaaS({
  database,
  config: {
    tenancy: { strategy: "shared" },
    database: { provider: "memory" }
  }
});

const org = await database.organizations.create({ name: "Acme", slug: "acme" });
await database.memberships.create({ organizationId: org.id, userId: "user_1", role: "owner" });

Verifying a custom adapter against the shared contract:

// my-adapter/src/my-adapter.contract.test.ts
import { runDatabaseAdapterContractTests } from "@saasifier/database/contract-tests";
import { createMyAdapter } from "./my-adapter.js";

runDatabaseAdapterContractTests("my-adapter", () => createMyAdapter(), {
  teardown: (db) => db /* close pool / reset state */ && undefined
});

API Reference

createMemoryAdapter(): DatabaseAdapter

Returns a fully in-memory DatabaseAdapter backed by Maps. Not tenant-isolated at the storage layer (everything lives in one process' memory) — it demonstrates the interface shape (every method scoped by organizationId), not physical separation. Intended for unit tests and local playgrounds, not production.

runDatabaseAdapterContractTests(name, createAdapter, options?): void

Registers a Vitest describe block (DatabaseAdapter contract: <name>) that exercises organization CRUD, cross-tenant membership isolation, atomic usage increments (including a concurrent-increment race test), audit log scoping, subscription upserts, webhook idempotency, and API key lifecycle/IDOR guards.

  • name: string — label used in the generated describe block.
  • createAdapter: () => Promise<DatabaseAdapter> | DatabaseAdapter — factory invoked before each test to get a fresh (or reset) adapter instance.
  • options.teardown?: (db: DatabaseAdapter) => Promise<void> | void — called after each test, e.g. to close a connection pool or truncate tables.

The DatabaseAdapter interface

DatabaseAdapter is a bag of scoped repositories:

interface DatabaseAdapter {
  organizations: OrganizationRepository;
  memberships: MembershipRepository;
  planAssignments: PlanAssignmentRepository;
  usage: UsageRepository;
  auditLogs: AuditLogRepository;
  subscriptions: SubscriptionRepository;
  webhookEvents: WebhookEventRepository;
  apiKeys: ApiKeyRepository;
}
  • OrganizationRepository — create, get, getBySlug, update, delete, list. There is intentionally no "list everything across tenants" beyond list() scoping conventions used elsewhere — every other repository takes an explicit organizationId.
  • MembershipRepository — list(organizationId), find(organizationId, userId), create, remove, updateRole.
  • PlanAssignmentRepository — get(organizationId) / set(organizationId, planKey). Tracks which plan an org is on; kept in sync by @saasifier/core's webhook handler and read by entitlements/usage rather than each inspecting subscriptions directly.
  • UsageRepository — get(organizationId, metric) and incrementIfWithinLimit(organizationId, metric, amount, limit), which atomically increments only if the result would stay within limit, returning the new total or null on refusal — real adapters implement this as a single UPDATE ... WHERE used + n <= limit rather than a racy read-then-write.
  • AuditLogRepository — create(entry) / list(organizationId).
  • SubscriptionRepository — get(organizationId) / upsert(subscription), insert-or-update keyed on organizationId.
  • WebhookEventRepository — hasProcessed(eventId) / markProcessed(event), the idempotency ledger checked before, and updated after, webhook side effects.
  • ApiKeyRepository — create, list(organizationId), revoke(organizationId, apiKeyId), touchLastUsed(apiKeyId), and findByHash(hashedSecret) — the one method that intentionally omits organizationId, since authenticating an inbound request starts from only the raw key; it receives a hash, never the raw secret, and the matched organization comes back as part of the ApiKey row.

Implement all eight repositories to plug a new storage backend into @saasifier/core, and run runDatabaseAdapterContractTests against it to confirm it honors the same guarantees as the memory adapter.