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

@objectifthunes/sandstone-sdk

v1.3.6

Published

Contracts-first backend toolkit for TypeScript + PostgreSQL + GraphQL. Framework-agnostic, vendor-agnostic.

Readme

@objectifthunes/sandstone-sdk

codecov

Contracts-first backend toolkit for TypeScript + PostgreSQL + GraphQL. Hexagonal architecture. Framework-agnostic, vendor-agnostic.

Why

Every Node.js backend rewrites the same boilerplate: database connections, auth, email, payments, GraphQL wiring, testing setup. And every framework locks you into its world.

This package defines TypeScript interfaces (ports) for every infrastructure concern and ships pure logic that depends only on those interfaces. Concrete implementations (adapters) are separate subpath exports with optional peer dependencies. Swap any piece without touching the rest.

The rule: if you delete any adapter and the core still compiles, the architecture is correct.

Architecture

ports/driving/     ← inbound (HttpHandler)
ports/driven/      ← outbound (DatabaseClient, Logger, TokenSigner, EmailTransport, ...)
src/auth|db|email| ← domain logic (depends only on ports)
adapters/          ← concrete implementations (pg, jose, stripe, supabase, r2, ...)
testing/in-memory/ ← test adapters for every port

Install

pnpm add @objectifthunes/sandstone-sdk

Then install only the adapters you need:

pnpm add pg jose resend                        # database, jwt, email
pnpm add stripe                                 # payments
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner  # storage
pnpm add pino                                   # logging

Quick Start

import { createAuth, createGraphQLHandler, createContextFactory, runMigrations } from '@objectifthunes/sandstone-sdk'
import { createPgClient } from '@objectifthunes/sandstone-sdk/pg'
import { createJoseSigner } from '@objectifthunes/sandstone-sdk/jose'
import { createResendTransport } from '@objectifthunes/sandstone-sdk/resend'
import { createPinoLogger } from '@objectifthunes/sandstone-sdk/pino'

const db = createPgClient({ connectionString: process.env.DATABASE_URL })
const tokens = createJoseSigner({ secret: process.env.JWT_SECRET })
const email = createResendTransport({ apiKey: process.env.RESEND_API_KEY })
const logger = createPinoLogger({ level: 'info' })

await runMigrations(db, { includeBuiltIn: true })

const auth = createAuth({ db, email, tokens, logger, otp: { length: 6, expiresIn: '10m' } })

await auth.sendCode('[email protected]')
const { user, tokens: pair, isNewUser } = await auth.verifyCode('[email protected]', '482917')

Ports (Interfaces)

The core defines 18 interfaces. Every module depends only on these.

| Port | Direction | What it does | |---|---|---| | HttpHandler | Driving (inbound) | (HttpRequest) => Promise<HttpResponse> | | DatabaseClient | Driven (outbound) | Query, transaction, health check | | Logger | Driven (outbound) | Structured logging with child loggers | | TokenSigner | Driven (outbound) | Sign/verify JWT tokens | | EmailTransport | Driven (outbound) | Send emails | | SmsTransport | Driven (outbound) | Send SMS to phone numbers (E.164 format) | | PushTransport | Driven (outbound) | Send push notifications to devices | | RealtimeProvider | Driven (outbound) | Publish events, presence, channel auth, disconnect | | PaymentProvider | Driven (outbound) | Customers, subscriptions, invoices, webhooks | | StorageProvider | Driven (outbound) | Upload, download, signed URLs | | CacheProvider | Driven (outbound) | Get/set/delete with TTL | | PasswordHasher | Driven (outbound) | Hash and verify passwords | | OAuthProvider | Driven (outbound) | Get OAuth redirect URL, handle callback | | Tracer | Driven (outbound) | Start spans, trace async operations | | SearchProvider | Driven (outbound) | Index, search, delete, vector search | | QueueProvider | Driven (outbound) | Enqueue, process, retry, dead letter queue | | FeatureFlagProvider | Driven (outbound) | Check flags, get values, targeting rules, percentage rollouts | | AuthorizationProvider | Driven (outbound) | Resource-level RBAC: can, authorize, grant, revoke, listPermissions |

Adapters

| Subpath | Port | Peer dep | What | |---|---|---|---| | ./pg | DatabaseClient | pg | PostgreSQL via pg driver | | ./supabase | DatabaseClient | pg | Supabase PostgreSQL (SSL + pooler defaults) | | ./jose | TokenSigner | jose | JWT — HS256, RS256, or ES256 | | ./pino | Logger | pino | Structured logging via Pino | | ./nodemailer | EmailTransport | nodemailer | SMTP email | | ./resend | EmailTransport | resend | Resend API | | ./twilio | SmsTransport | (none, uses fetch) | Twilio Programmable Messaging | | ./sns | SmsTransport | @aws-sdk/client-sns | AWS SNS SMS | | ./fcm | PushTransport | (none, uses fetch + jose) | Firebase Cloud Messaging (FCM v1) | | ./apns | PushTransport | (none, uses node:http2) | Apple Push Notification service (APNs) | | ./socketio | RealtimeProvider | socket.io | Socket.io — bidirectional WebSocket events | | ./pusher | RealtimeProvider | (none, uses fetch) | Pusher Channels — managed WebSocket events | | ./ably | RealtimeProvider | (none, uses fetch) | Ably — managed pub/sub and presence | | ./argon2 | PasswordHasher | argon2 | Argon2id password hashing | | ./stripe | PaymentProvider | stripe | Stripe SDK | | ./paddle | PaymentProvider | (none, uses fetch) | Paddle Billing API + webhook verification | | ./lemonsqueezy | PaymentProvider | (none, uses fetch) | Lemon Squeezy API + webhook HMAC | | ./revenuecat | PaymentProvider | (none, uses fetch) | RevenueCat API + webhook HMAC | | ./s3 | StorageProvider | @aws-sdk/client-s3 | S3-compatible storage | | ./r2 | StorageProvider | @aws-sdk/client-s3 | Cloudflare R2 (S3-compat with R2 defaults) | | ./local-storage | StorageProvider | (none) | Local filesystem | | ./memory | CacheProvider | (none) | In-process LRU cache with TTL | | ./redis | CacheProvider | ioredis | Redis over TCP via ioredis | | ./upstash | CacheProvider | @upstash/redis | Upstash Redis (HTTP) | | ./oauth-google | OAuthProvider | (none, uses fetch) | Google OAuth 2.0 | | ./oauth-github | OAuthProvider | (none, uses fetch) | GitHub OAuth 2.0 | | ./oauth-apple | OAuthProvider | (none, uses fetch) | Sign in with Apple | | ./oauth-discord | OAuthProvider | (none, uses fetch) | Discord OAuth 2.0 | | ./otel | Tracer | @opentelemetry/api | OpenTelemetry tracing | | ./meilisearch | SearchProvider | meilisearch | Meilisearch full-text + vector search | | ./typesense | SearchProvider | typesense | Typesense full-text + vector search | | ./pg-search | SearchProvider | pg | PostgreSQL full-text + pgvector search | | ./bullmq | QueueProvider | bullmq | BullMQ — Redis-based job queue | | ./pg-boss | QueueProvider | pg-boss | pg-boss — PostgreSQL-based job queue | | ./node-cron | TickSource | node-cron | Precise cron scheduling | | ./pg-flags | FeatureFlagProvider | pg | PostgreSQL-backed feature flags with targeting rules | | ./env-flags | FeatureFlagProvider | (none) | Environment variable feature flags — zero infra | | ./launchdarkly | FeatureFlagProvider | @launchdarkly/node-server-sdk | LaunchDarkly SDK integration | | ./pg-authz | AuthorizationProvider | pg | PostgreSQL-backed RBAC (migration 018) | | ./casbin | AuthorizationProvider | casbin | Casbin policy engine — RBAC, ABAC, domain models | | ./adapters | HttpHandler | (optional peers) | Express, Fastify, Hono, raw Node.js | | ./nestjs | All ports | @nestjs/common | NestJS dynamic module + injection tokens | | ./dev | All ports | (none) | In-memory dev adapter — mirrors all production factory names. Zero config. | | ./testing | All ports | (none) | In-memory implementations for every port |

Modules

Database

  • Query buildersql tagged template, select/insert/update/del builders
  • Repository factory — generic CRUD with pagination, soft delete, typed queries
  • Migrations — plain SQL files, built-in tables for auth/payments/email

Auth

  • OTP — 6-digit code sent to email, SHA-256 hashed in DB, single-use, rate-limited
  • Password — register, login, change password, reset via OTP; exponential lockout on failed attempts
  • OAuth — Google, GitHub, Apple, Discord (CSRF-protected state parameter); extensible to any provider via OAuthProvider
  • JWT access + refresh tokens with DB-backed sessions
  • Role-based guards

GraphQL

  • Code-first schema helpers, custom scalars (DateTime, JSON, UUID, Email)
  • Context factory with automatic auth extraction and dataloaders
  • Framework-agnostic HTTP handler

Email

  • Template engine (HTML with {{variable}} interpolation)
  • DB-backed queue with retries
  • Built-in templates: login code, welcome, invoice

SMS & Push Notifications

  • SmsTransport — send SMS to E.164 phone numbers (send({ to, body }))
  • PushTransport — send single or bulk push notifications to device tokens (send, sendMany)
  • SMS adapters: Twilio (./twilio, fetch-based), AWS SNS (./sns, @aws-sdk/client-sns)
  • Push adapters: FCM v1 (./fcm, fetch + jose for OAuth 2.0), APNs (./apns, node:http2 + JWT)

Real-Time

  • RealtimeProvider — publish events to channels, query presence, authorize private channels, disconnect clients
  • publish(channel, event, data) — broadcast to all subscribers on a channel
  • getChannels() / getSubscriberCount(channel) / getSubscribers(channel) — presence queries
  • authorizeChannel(socketId, channel, userData?) — sign private/presence channel auth tokens
  • disconnect?(socketId) — optional: force-disconnect a specific client
  • Adapters: Socket.io (./socketio, socket.io), Pusher (./pusher, fetch-based), Ably (./ably, fetch-based)

Payments

  • Customer sync (DB + provider), subscription lifecycle, webhook routing, invoice tracking
  • Timing-safe webhook signature verification

Search

  • Keyword search with filters, facets, highlights, sorting, and pagination
  • Optional vector/semantic search with hybrid keyword+vector scoring
  • Adapters: Meilisearch, Typesense, PostgreSQL full-text search (+ pgvector)

Feature Flags

  • isEnabled(flag, context?), getValue(flag, defaultValue, context?), getAllFlags(context?)
  • FlagContext for per-user or per-tenant targeting (userId, tenantId, attributes)
  • Targeting rules by user ID list, tenant ID list, or percentage rollout
  • Adapters: PostgreSQL (./pg-flags), environment variables (./env-flags), LaunchDarkly (./launchdarkly)

Authorization

  • Resource-level RBAC via AuthorizationProvidercan, authorize, grant, revoke, listPermissions
  • Subject + action + resource type + optional resource ID (type-level or instance-level)
  • manage action expands to all actions on a resource type
  • DB-backed via the built-in authorization_permissions table (migration 018) with '*' sentinel for type-level grants
  • Adapters: PostgreSQL (./pg-authz), Casbin (./casbin)

Audit Logging

  • createAuditLogger() — append-only log of who did what, when, and to which resource
  • log(entry) / query(filters, pagination) / count(filters) — structured entries with actor, action, resource, and metadata
  • DB-backed via the built-in audit_log table (migration 017) with indexes on actor_id, resource_type, and created_at

Background Jobs & Scheduling

  • Durable job queue with retries, backoff, dead letter queue
  • Composable scheduler with pluggable tick sources (node-cron, setInterval)
  • Adapters: BullMQ (Redis), pg-boss (PostgreSQL)

Dashboard

  • createDashboard(app, options?) — self-contained HTML dashboard served as an HttpHandler
  • Shows: port wiring map, registered routes, health status, email queue depth, scheduler tasks, feature flags, realtime channels, recent audit trail, authorization stats
  • Dark theme, auto-refresh via polling, optional shared-secret auth guard
  • No React, no build step — pure inline HTML/CSS/JS; mount at any route

HTTP

  • CORS middleware, rate limiter, health checks — all return HttpHandler functions

NestJS Integration

import { TpgModule } from '@objectifthunes/sandstone-sdk/nestjs'
import { createSupabaseClient } from '@objectifthunes/sandstone-sdk/supabase'
import { createJoseSigner } from '@objectifthunes/sandstone-sdk/jose'
import { createResendTransport } from '@objectifthunes/sandstone-sdk/resend'
import { createRevenueCatProvider } from '@objectifthunes/sandstone-sdk/revenuecat'
import { createR2Storage } from '@objectifthunes/sandstone-sdk/r2'
import { createUpstashCache } from '@objectifthunes/sandstone-sdk/upstash'
import { createPinoLogger } from '@objectifthunes/sandstone-sdk/pino'

@Module({
  imports: [
    TpgModule.forRoot({
      db: createSupabaseClient({ connectionString: process.env.DATABASE_URL }),
      logger: createPinoLogger({ level: 'info' }),
      tokens: createJoseSigner({ secret: process.env.JWT_SECRET }),
      email: { transport: createResendTransport({ apiKey: '...' }), from: '[email protected]' },
      auth: { otp: { length: 6, expiresIn: '10m' } },
      payments: { provider: createRevenueCatProvider({ apiKey: '...', webhookSecret: '...' }) },
      storage: createR2Storage({ accountId: '...', accessKeyId: '...', secretAccessKey: '...', bucket: '...' }),
      cache: createUpstashCache({ url: '...', token: '...' }),
      runMigrations: { includeBuiltIn: true },
    }),
  ],
})
export class AppModule {}

Then inject anywhere:

import { Inject } from '@nestjs/common'
import { TPG_DB, TPG_AUTH, TPG_TOKENS } from '@objectifthunes/sandstone-sdk/nestjs'
import type { DatabaseClient, Auth } from '@objectifthunes/sandstone-sdk'

@Injectable()
export class MyService {
  constructor(
    @Inject(TPG_DB) private db: DatabaseClient,
    @Inject(TPG_AUTH) private auth: Auth,
  ) {}
}

Testing

In-memory implementations for every port ship with the package. Real logic, zero external dependencies.

import { createAuth } from '@objectifthunes/sandstone-sdk'
import {
  createInMemoryDatabase,
  createInMemoryEmail,
  createInMemoryTokenSigner,
  createInMemoryLogger,
} from '@objectifthunes/sandstone-sdk/testing'

const db = createInMemoryDatabase()
const email = createInMemoryEmail()
const tokens = createInMemoryTokenSigner()
const logger = createInMemoryLogger()

const auth = createAuth({ db, email, tokens, logger, otp: { length: 6, expiresIn: '10m' } })

await auth.sendCode('[email protected]')
const code = email.lastCodeSent()
const { user } = await auth.verifyCode('[email protected]', code!)

Hard Dependencies

Only three: graphql (the spec), zod (config validation), dataloader (batching). Everything else is an optional peer dep of an adapter.

Security Considerations

JWT algorithm: The jose adapter supports HS256 (symmetric, default), RS256, and ES256. HS256 is appropriate for single-service deployments where the same process signs and verifies tokens. For multi-service deployments where services verify tokens without sharing the signing secret, use RS256 or ES256 by passing { privateKey, publicKey, algorithm: 'RS256' } (or 'ES256') to createJoseSigner.

CSRF: The auth module protects OAuth flows with state parameters. Password login and OTP verification do not include CSRF tokens. If your application uses cookie-based sessions (rather than Bearer tokens), you must add CSRF protection at the framework level (e.g., csurf for Express, built-in CSRF for NestJS).

Query builder: Column names, table names, and identifiers passed to the query builder are validated against /^[a-zA-Z_][a-zA-Z0-9_]*$/. Never pass user input as an identifier — only use hardcoded strings.

License

MIT