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

vaultmd

v0.4.0

Published

Headless markdown-vault data layer for Bun — CRUD + SQLite index, no Obsidian required

Readme

vaultmd

A headless markdown-vault data layer for Bun — CRUD over .md notes plus a derived SQLite index for collection queries, backlinks, and full-text search. No Obsidian, no Electron, no plugin.

npm runtime: Bun license: MIT status: published docs

📖 Documentation & API reference

vaultmd is an npm package that gives your Bun app a programmatic data layer over a folder of markdown notes. Your .md files on disk stay the single source of truth; vaultmd maintains a rebuildable bun:sqlite index alongside them so you can query notes by tag or frontmatter, walk backlinks, and run keyword search — all without an editor, sync engine, or background daemon.

It's the engine, not the app: generic vault mechanics only. Personas, domain schemas, and sync logic live in whatever you build on top.

Status

Released (0.4.0) — live on npm. The public API is frozen and tested, and the package ships as a bundled dist/ (ESM + types). Being 0.x, the surface may still evolve before 1.0; see CHANGELOG.md for what changed.

Features

  • CRUD over markdown — create, read, update, delete .md notes with flat YAML frontmatter.
  • Derived SQLite index — a rebuildable cache, never the source of truth. Delete it and it rebuilds from disk.
  • Collection queries — filter notes by tag, frontmatter field, or folder; order and paginate.
  • Backlinks & outbound links[[wikilink]] or relative-link resolution.
  • Full-text search — keyword search over note bodies (SQLite FTS5) with highlighted snippets.
  • Write-through indexing — every mutation updates the index inside the same per-file lock as the file write; the two never drift.
  • Concurrency-safe — in-process mutex plus optional cross-process lockfiles guard concurrent writers.
  • Scoped access — per-instance read/write path allowlists make it safe to hand different parts of the vault to different consumers.
  • TypeScript-first — full types, a small frozen public surface, and lower level primitives exported for advanced use.

Requirements

  • Bun ≥ 1.1.0. vaultmd uses bun:sqlite, Bun.file, and other Bun built-ins — it does not run under Node.

Install

bun add vaultmd

Quick start

import { createVault } from 'vaultmd';

const vault = await createVault({
  root: '/path/to/vault',
  // Read everything, but only write under Notes/.
  prefixes: { read: [''], write: ['Notes/'] },
  // The index db lives in a DATA dir, NOT inside the synced vault.
  indexPath: './data/vault-index.db',
});

// Create a note with frontmatter + body.
await vault.notes.createNote('Notes/today.md', {
  frontmatter: { tags: ['project', 'daily'], status: 'open' },
  body: '# Today\n\nSee [[roadmap]] for context.\n',
});

// Query the collection.
const open = vault.query.queryNotes({
  tag: 'project',
  where: { status: 'open' },
  orderBy: { field: 'mtime_ms', dir: 'desc' },
  limit: 20,
});

// Walk the link graph.
const incoming = vault.query.backlinks('Notes/roadmap.md');

// Full-text search.
const hits = vault.query.searchText('context');

// Append to a note (atomic, write-through indexed).
await vault.notes.updateNote('Notes/today.md', { append: '\n- shipped readme' });

vault.close();

Concepts

Files are the source of truth; the index is a cache. Every note is a plain .md file you can edit by hand, sync with git or Dropbox, or open in any editor. The SQLite index is derived from those files and can be rebuilt at any time (vault.rebuild()), so it never has to be backed up or trusted over disk.

Read/write scopes. prefixes.read and prefixes.write are path-prefix allowlists. An empty string ('') means "the whole vault". Queries only ever return notes the instance is allowed to read; writes are rejected outside the write scope. This is the security chokepoint — all path canonicalization and containment checks live behind it.

Write-through indexing. createNote, updateNote, editFrontmatter, transformNote, and deleteNote update the index inside the same per-file lock as the file write. The file and its index row are never updated in separate transactions, so a crash can't leave them disagreeing.

Lazy reconcile. Reads stay synchronous. The first read (and the first after each TTL window) fires a single background sweep that picks up any out-of-band edits — files you changed in your editor while the process was running. The result is visible to the next read; a failed sweep never breaks a read.

Index location. The .db file and its -wal / -shm sidecars must live in a data directory outside the synced vault, and stay gitignored (*.db*). Never let the cache get synced as if it were content.

API

The only public entry point is createVault. Everything below hangs off the Vault it returns.

createVault(config): Promise<Vault>

| Option | Type | Default | Description | | ------------------------ | -------------------------------------- | ------------ | --------------------------------------------------------------------------- | | root | string | (required) | Absolute path to the vault directory. | | prefixes | { read: string[]; write: string[] } | (required) | Read/write path-prefix allowlists ('' = whole vault). | | indexPath | string | (required) | Where the SQLite index lives. Keep it out of the vault. | | caseSensitive | boolean | (auto) | Override filesystem case sensitivity detection. | | ignore | string[] | [] | Glob patterns to exclude from indexing. | | linkResolution | 'wikilink' \| 'relative' | 'wikilink' | How links are extracted and resolved. | | lazyReconcile | boolean | true | Fire background reconcile sweeps on read. | | reconcileTtlMs | number | 2000 | Minimum gap between lazy sweeps. | | sqliteBusyTimeoutMs | number | 5000 | SQLite busy timeout / cross-process lock wait. | | crossProcessWriterLock | boolean | true | Guard writes with cross-process lockfiles. | | onCommit | (e: CommitEvent) => void \| Promise | — | Hook fired after each committed mutation (e.g. to mirror changes upstream). |

vault.notes

// Read a note; pass { withLinks: true } to include outbound + backlinks.
readNote(path, opts?: { withLinks?: boolean }): Promise<ReadNoteResult>

// Create a note. Throws ALREADY_EXISTS rather than clobbering.
createNote(path, input: { frontmatter?: Record<string, unknown>; body: string }): Promise<void>

// Mutate body: append text, or replace an exact, unambiguous match.
updateNote(path, op: { append: string } | { editByMatch: { old: string; new: string } }): Promise<void>

// Edit flat frontmatter via a mutator callback. Returns 'edited' | 'unchanged' | 'unverifiable'.
editFrontmatter(path, mutate: (fm: Record<string, unknown>) => void): Promise<EditOutcome>

// Transform a note's FULL content atomically. Return new content, or null for a
// no-op. Never creates a missing file (throws REFUSE_CREATE). The callback must
// be pure — it is re-invoked on write contention. Returns 'edited' | 'unchanged'.
transformNote(path, transform: (current: string | null) => string | null): Promise<TransformOutcome>

// Delete a note. Returns whether a file was actually removed.
deleteNote(path): Promise<boolean>

ReadNoteResult is { frontmatter, tags, body, valid, outbound?, backlinks? }, where valid is 'flat' | 'present-but-invalid' | 'none'.

vault.query

// Filter the collection. Returns NoteHit[] = { path, title, frontmatter, tags }[].
queryNotes(opts?: {
  tag?: string;
  where?: Record<string, string | number | boolean>; // frontmatter equality
  folder?: string;
  orderBy?: { field: 'mtime_ms' | 'path' | 'title'; dir: 'asc' | 'desc' };
  limit?: number;
  offset?: number;
}): NoteHit[]

// Notes linking TO this path. Returns { from: string }[].
backlinks(path, opts?: { limit?: number; offset?: number }): Backlink[]

// Links FROM this path. Returns { target, resolved }[] (resolved is null if dangling).
outboundLinks(path, opts?: { limit?: number; offset?: number }): OutboundLink[]

// Full-text keyword search over bodies. Returns { path, title, snippet? }[].
searchText(q, opts?: { tag?: string; folder?: string; limit?: number; offset?: number }): SearchHit[]

// Existing tags ranked by use. Returns TagInfo[] = { tag, count }[] (count desc, tag asc).
// prefix = case-sensitive hierarchy prefix; contains = ASCII case-insensitive substring.
tags(opts?: { prefix?: string; contains?: string; folder?: string; limit?: number }): TagInfo[]

Lifecycle

vault.reconcile(): Promise<void>            // full sweep now (vs. lazy)
vault.reconcilePaths(rels: string[]): Promise<void>  // reconcile specific paths
vault.rebuild(): Promise<void>              // drop & rebuild the index from disk
vault.close(): void                         // close the db handle

Lower-level primitives

For advanced use, the package also exports the building blocks createVault assembles — the IO chokepoint, atomic locked-file transforms, and the pure frontmatter / link parsers:

import {
  createVaultIo,           // path → safe-IO security layer
  withFileTransform,       // atomic compare-and-swap file edit
  withFileDelete,          // atomic delete with commit hook
  parseFrontmatter,        // pure flat-YAML frontmatter parser
  editFrontmatter,         // pure frontmatter editor
  serializeFrontmatter,    // flat map → fenced YAML block (inverse of parse)
  isFlatFrontmatter,
  deriveTags,
  extractLinks,            // pull wikilinks / relative links from text
  storedLinksFor,
} from 'vaultmd';

Error handling

Every failure throws an MdVaultError carrying a stable code. Catch and switch on err.code, never on the message:

import { MdVaultError } from 'vaultmd';

try {
  await vault.notes.createNote('Notes/today.md', { body: '...' });
} catch (err) {
  if (err instanceof MdVaultError && err.code === 'ALREADY_EXISTS') {
    // handle the clash
  } else {
    throw err;
  }
}

Codes: ALLOWLIST_VIOLATION, NOT_MARKDOWN, NOT_FOUND, ALREADY_EXISTS, NO_MATCH, AMBIGUOUS_MATCH, MTIME_CONFLICT, REFUSE_CREATE, FRONTMATTER_INVALID, VALIDATION_ERROR, COMMIT_FAILED, INDEX_UNAVAILABLE.

Development

bun install
bun test            # full suite
bun run check       # biome check . && tsc --noEmit — the authoritative gate
bun run format      # biome format --write .

Run bun run check before sending a change; it's green/red, not advisory.

License

MIT © Ivan Kalinichenko