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

@noy-db/hub

v0.6.0

Published

Zero-knowledge, offline-first, encrypted document store — core library with AES-256-GCM, PBKDF2, multi-user keyring, and sync engine

Readme

@noy-db/hub

Zero-knowledge, offline-first, encrypted document store — core library.

npm license

Part of noy-db"None Of Your Damn Business". This is the core library — install this first, then pair it with a storage backend.

Install

Every noy-db app needs @noy-db/hub plus at least one storage backend (to-*). Everything else is optional.

pnpm add @noy-db/hub @noy-db/to-memory

Pick a storage backend — @noy-db/to-*

| Package | Use for | |---------|---------| | to-memory | Tests, prototypes, ephemeral data | | to-file | Local disk, USB stick | | to-browser-idb | Browser IndexedDB (atomic CAS) | | to-browser-local | Browser localStorage | | to-aws-dynamo | DynamoDB single-table | | to-aws-s3 | S3 object store | | to-cloudflare-d1 | Edge SQLite via Workers | | to-cloudflare-r2 | Zero-egress object storage | | to-postgres | PostgreSQL jsonb | | to-mysql | MySQL / MariaDB JSON | | to-sqlite | better-sqlite3 / node:sqlite / bun:sqlite | | to-supabase | Supabase Postgres + Storage | | to-turso | Hosted libSQL (replicated SQLite) | | to-webdav | Nextcloud / ownCloud | | to-ssh | Remote SFTP backend | | to-smb | Windows shares / NAS | | to-nfs | NFS mounts | | to-icloud | iCloud Drive (.icloud-aware) | | to-drive | Google Drive bundle | | to-meter | Wrap any store with metrics |

Optional ecosystem

  • Framework integrations@noy-db/in-*: vue, pinia, nuxt, react, nextjs, svelte, solid, zustand, tanstack-query, tanstack-table, yjs, ai, rest.
  • Authentication paths@noy-db/on-*: webauthn, oidc, totp, email-otp, magic-link, recovery, shamir, pin, threat.
  • Export formats@noy-db/as-*: csv, json, ndjson, xml, sql, xlsx, blob, zip, noydb (encrypted bundle).
  • Session-share transports@noy-db/by-*: tabs (BroadcastChannel multi-tab), peer (WebRTC).
  • CLI tooling@noy-db/cli (noydb binary; inspect/verify .noydb files), create-noy-db (npm create noy-db scaffolder).

Quick start

import { createNoydb } from '@noy-db/hub'
import { memory } from '@noy-db/to-memory'

type Invoice = { id: string; amount: number; customer: string }

const db = await createNoydb({
  store: toMemory(),
  userId: 'alice',
  secret: 'correct horse battery staple',
})

const acme = await db.openVault('acme')
const invoices = acme.collection<Invoice>('invoices')

await invoices.put('INV-001', { id: 'INV-001', amount: 8500, customer: 'ABC Trading' })
const all = await invoices.list()

What it does

  • Zero-knowledge encryption — AES-256-GCM + PBKDF2 (600K iterations) + AES-KW, all via Web Crypto API
  • Per-collection keys — one DEK per collection, wrapped with a per-user KEK
  • Multi-user access control — owner, admin, operator, viewer, client roles
  • Offline-first sync — push/pull with optimistic concurrency on encrypted envelopes
  • Audit history — full-copy snapshots with history(), diff(), revert(), pruneHistory()
  • Zero runtime dependencies

Cross-vault queries

When a single principal holds grants across many vaults — multi-tenant apps, multi-project setups, multi-workspace tools — there are two APIs for enumerating and fanning out across them:

db.listAccessibleVaults(options?) — enumerate

Returns every vault the calling principal can unwrap, optionally filtered by minimum role. The walk is bounded by the local keyring index — vaults where the user has no keyring file or where the secret doesn't unwrap are silently dropped from the result.

// All vaults I can unlock
const all = await db.listAccessibleVaults()
// → [{ id: 'T1', role: 'owner' }, { id: 'T7', role: 'admin' }, ...]

// Only vaults where I'm at least admin
const admin = await db.listAccessibleVaults({ minRole: 'admin' })

Existence-leak guarantee. The return value never reveals the existence of a vault the caller cannot unwrap. The store sees the enumeration call (it owns the storage), but downstream consumers of listAccessibleVaults() only see the filtered list.

Store capability. Requires the optional NoydbStore.listVaults() method. The @noy-db/to-memory and @noy-db/to-file stores implement it; cloud stores (@noy-db/to-aws-dynamo, @noy-db/to-aws-s3) and @noy-db/to-browser-idb do not (cloud enumeration needs a GSI or list-bucket permission that has to be configured by the consumer). Calling listAccessibleVaults() against a store that doesn't implement listVaults throws StoreCapabilityError. Workaround: maintain the candidate list out of band and pass it directly to queryAcross().

db.queryAcross(ids, fn, options?) — fan out

Runs a per-vault callback against a list of vault ids and collects the results, tagged by vault. Per-vault errors do not abort the others — each result slot carries either result or error.

const accessible = await db.listAccessibleVaults({ minRole: 'admin' })

const results = await db.queryAcross(
  accessible.map((v) => v.id),
  async (vault) => {
    return vault.collection<Invoice>('invoices').query()
      .where('month', '==', '2026-03')
      .toArray()
  },
  { concurrency: 4 }, // default 1 — bump for cloud stores
)
// results: Array<{ vault, result?: Invoice[], error?: Error }>

Composes with exportStream() for cross-vault plaintext export:

await db.queryAcross(accessible.map((v) => v.id), async (vault) => {
  const out: unknown[] = []
  for await (const chunk of vault.exportStream()) out.push(chunk)
  return out
})

Backup and export

noy-db ships two distinct paths for getting data out of a vault. They are not interchangeable — use the one that matches your goal.

vault.dump() — encrypted backup (the default)

Produces a tamper-evident encrypted JSON envelope. Records stay encrypted, the hash-chained ledger is included so the receiver can verify integrity end-to-end after load(), and the recipient must hold a valid keyring to read anything. Use this for backup, transport between machines, or any scenario where the data must remain protected on disk.

const backup = await acme.dump()                // string of encrypted JSON
await fs.writeFile('./acme-backup.json', backup) // safe to store anywhere
// later, on another machine:
await otherAcme.load(backup)                    // verifies + restores

vault.exportStream() and vault.exportJSON() — plaintext export

These methods decrypt your records and produce plaintext.

exportStream() is an authorization-aware async generator that yields per-collection chunks of decrypted records, with schema and ref metadata attached. exportJSON() is a five-line wrapper that serializes the stream to a single JSON string.

Both methods are ACL-scoped: collections the calling principal cannot read are silently skipped. An operator with { invoices: 'rw' } permissions on a five-collection vault exports only invoices, with no error on the others.

// Stream every collection the caller can read
for await (const chunk of acme.exportStream()) {
  console.log(chunk.collection, chunk.records.length)
}

// Or get a single JSON string
const json = await acme.exportJSON()
await fs.writeFile('./backup.json', json)

Use only when:

  • You are the authorized owner of the data, and
  • You have a legitimate downstream tool that requires plaintext, and
  • You have a documented plan for how the resulting plaintext will be protected and eventually destroyed.

If your goal is encrypted backup or transport between noy-db instances, use vault.dump() instead.

Why no built-in file path support

Core has zero node: imports — it runs unchanged in browsers, Node, Bun, Deno, and edge runtimes. exportJSON() returns a Promise<string> so the consumer chooses any sink (fs.writeFile, Blob download, fetch upload, IndexedDB) and the destination decision stays explicit at the call site. This is also better for the security warning: there's no library function quietly writing plaintext somewhere.

Other plaintext formats

CSV, XML, xlsx, and the rest of the plaintext tier — plus encrypted .noydb bundles under the as-noydb encrypted tier — all live in the @noy-db/as-* family. Every invocation is gated by the two-tier authorization model (canExportPlaintext default off, canExportBundle default on for owner/admin) and lands in the audit ledger.

Money fields

money() is a schema-layer field descriptor (a sibling of i18nText() / dictKey()) for currency-safe, exact decimal values. Money is stored as a scaled integer encoded as a digit string, so it is exact for any magnitude — past Number.MAX_SAFE_INTEGER included (a JSON number would silently truncate at 2^53).

vault.collection('invoices', {
  schema: z.object({ id: z.string(), total: z.union([z.number(), z.string()]) }),
  moneyFields: { total: money({ currency: 'EUR', scale: 2 }) }, // scale optional — ISO-4217 default
})

await invoices.put('a', { id: 'a', total: '123.45' })          // stored as '12345'
const inv = await invoices.get('a', { locale: 'de-DE' })
// inv.total           → '123.45'   (exact decimal string)
// inv.totalFormatted  → '123,45 €' (Intl, full precision)
// inv.totalNumber     → 123.45     (convenience JS number; lossy past 2^53)

// Exact aggregation — sum/min/max run in BigInt, no float drift:
invoices.query().aggregate({ total: sum('total') }).run() // → '0.60', never 0.6000000000000001
  • Rounding: excess precision is rejected by default; opt in per field with money({ ..., rounding: 'half-even' }) (half-up / half-even / half-down / up / down / ceil / floor).
  • Multi-currency: opt in with money({ currencies: 'any' | ['EUR','USD'] }) — currency travels per record as { amount, currency }; sum returns an exact per-currency map ({ EUR: '15.50', USD: '3.00' }), or one figure with sum('total', { convertTo: 'EUR', fx }).
  • Money sum/min/max implement incremental remove(), so they stay exact under live aggregation and materialized-view maintenance.

Computed fields

computed declares schema-owned scalar fields derived on write — keeping the arithmetic next to the schema instead of scattered across handlers. Each function is pure and synchronous; they run first in the write pipeline (before schema validation), in declaration order, so a later field can read an earlier one. The result is materialized on the record — stored, queryable, and aggregate(sum())-able like any field.

vault.collection('lines', {
  schema: z.object({
    id: z.string(), unitPrice: z.number(), qty: z.number(),
    netAmount: z.number().optional(), taxAmount: z.number().optional(), total: z.number().optional(),
  }),
  computed: {
    netAmount: (r) => r.unitPrice * r.qty,
    taxAmount: (r) => r.netAmount * 0.22,   // reads the field computed above
    total:     (r) => r.netAmount + r.taxAmount,
  },
})

await lines.put('a', { id: 'a', unitPrice: 10, qty: 3 })  // computed fields not supplied
const line = await lines.get('a')   // → { …, netAmount: 30, taxAmount: 6.6, total: 36.6 }
  • A computed field overwrites any user-supplied value of the same name (the field is schema-owned); a throwing function rejects the write with ComputedFieldError.
  • Composes with money() — declare a computed field as a money field too and it's quantized after evaluation, so sum() over it is exact.

Immutable collections (WORM)

immutableGuard makes a collection write-once after a condition holds — issued invoices/DDTs that must never change. It's declarative sugar over guards: it generates the block-on-check/onDelete + ledgered admin-amendment strategy, so it reuses the whole guard machinery (and composes with periods/history).

import { createNoydb, immutableGuard } from '@noy-db/hub'

await createNoydb({
  store, user, secret,
  guardStrategies: [
    immutableGuard({ collection: 'invoices', after: (r) => r.status === 'issued' }),
  ],
})

await invoices.put('a', { id: 'a', status: 'draft',  total: 100 }) // ok
await invoices.put('a', { id: 'a', status: 'issued', total: 100 }) // ok — the transition write
await invoices.put('a', { id: 'a', status: 'issued', total: 999 }) // ✗ RecordLockedError
await invoices.delete('a')                                          // ✗ RecordLockedError

// the sanctioned, ledgered override:
await db.transaction({ amendment: true, reason: 'correct issued total' }, async (tx) => {
  tx.vault('books').collection('invoices').put('a', { id: 'a', status: 'issued', total: 110 })
})
  • after(record) is evaluated on the existing record, so inserts and the write that first makes a record immutable are allowed; everything after is blocked.
  • appendOnly: true is shorthand for after: () => true — immutable from creation.
  • The admin/owner amendment path is the only way through, and every amendment is appended to the audit ledger.

Retention, legal-hold & archival

For retention-bound data (e.g. 10-year fiscal records), two facilities share one rule — a legal hold blocks eviction.

Blob retention (vault.compact()) gains a hold and a period-bound floor:

vault.collection('invoices', {
  blobFields: {
    pdf: {
      retainDays: 3650,                              // base TTL
      legalHold:  (r) => r.underLitigation === true, // never evict while held
      retainUntil:(r) => r.fiscalYearEnd,            // floor: keep until period obligation ends
    },
  },
})
const { evicted, held } = await vault.compact() // held = retained-by-hold count

Record archival (withArchive) relocates sealed records to a cold store — envelope-level, no re-encryption — and restores on demand:

import { createNoydb, withArchive } from '@noy-db/hub'

const db = await createNoydb({ store: primary, archiveStrategy: withArchive({ store: coldStore }) })
vault.collection('invoices', {
  archive: { archiveWhen: (r) => r.fiscalYear <= thisYear - 1, legalHold: (r) => r.underHold },
})

await vault.archive()                       // → { archived, held, scanned }
await vault.listArchived('invoices')        // → [{ collection, id }, …]
await vault.restore('invoices', 'inv-2020') // relocate back to primary (decryptable)

Archival uses low-level relocation, so it bypasses guards (issued/immutable records over a sealed period can still be archived) and doesn't recompute finalized aggregates. Archived records read null from the primary store until restored; a legalHold predicate blocks archival entirely.

Atomic sequences

vault.sequence(name) gives gap-free, exactly-once numbering — the primitive fiscal/ERP/ticketing apps need for invoice or DDT numbers — backed by an optimistic compare-and-swap counter.

const n = await vault.sequence('invoice-2026').next()   // 1, then 2, 3, … no gaps, no duplicates
const cur = await vault.sequence('invoice-2026').peek()  // read current value without allocating
  • Independent per namesequence('invoice-2026') and sequence('ddt-2026') are separate counters.
  • Concurrency-safe — concurrent next() calls retry on CAS contention (jittered backoff); a genuine burst beyond the retry budget surfaces SequenceContentionError so the caller can retry or queue.
  • Online-only — by design. Gap-free numbering needs single-authority serialization, which an offline writer can't provide. next() throws SequenceOfflineError unless the store advertises capabilities.casAtomic. This is the honest wall: assign each next() value to its record in the same operation (a discarded value is a gap in usage, not in the sequence).

Status

Pre-release (0.1.0-pre.1). API may change before 1.0. Install from the next dist-tag:

pnpm add @noy-db/hub@next @noy-db/to-memory@next

Documentation

License

MIT © vLannaAi