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

pglite-encrypted-fs

v0.2.0

Published

Encrypted filesystem for PGlite — AES-256-GCM page-level encryption for PostgreSQL databases

Readme

pglite-encrypted-fs

npm version CI License: MIT

An encrypted virtual filesystem for PGlite. Provides transparent AES-256-GCM page-level encryption so your PGlite database files are encrypted at rest.

Status: Alpha. The on-disk format is not yet versioned and may change before 1.0.

Why this exists

PGlite gives you a full embedded PostgreSQL in Node.js -- SQL, indexes, transactions, extensions like pgvector, all without a server. But out of the box, database files sit plaintext on disk.

This package is an encrypted VFS that plugs into PGlite's filesystem layer, encrypting every page as it's written and decrypting as it's read. Your PGlite code stays the same -- you just pass an EncryptedFS instance at creation.

Features

  • AES-256-GCM authenticated encryption -- page-level encryption with integrity verification on every read
  • PBKDF2-SHA512 key derivation -- 256K iterations, aligned with OWASP Password Storage Cheat Sheet guidance
  • Near-zero read overhead -- decrypted pages are cached in PostgreSQL's buffer pool; subsequent reads hit the cache
  • AAD binding prevents page swapping/replay attacks -- each page is bound to its file identity and position
  • Passphrase verification on reopen -- a wrong key is detected immediately, before any data is served
  • Transparent to PGlite extensions -- pgvector and other extensions work normally on top of the encrypted VFS

Install

pglite-encrypted-fs requires @electric-sql/pglite as a peer dependency.

# pnpm
pnpm add pglite-encrypted-fs @electric-sql/pglite

# npm
npm install pglite-encrypted-fs @electric-sql/pglite

# yarn
yarn add pglite-encrypted-fs @electric-sql/pglite

Quick Start

import { PGlite } from '@electric-sql/pglite'
import { EncryptedFS } from 'pglite-encrypted-fs'

const dataDir = './my-encrypted-db'
const fs = new EncryptedFS(dataDir, 'my-secret-passphrase')
const db = await PGlite.create({ dataDir, fs })

await db.exec('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)')
await db.exec("INSERT INTO users (name) VALUES ('Alice')")
const result = await db.query('SELECT * FROM users')
console.log(result.rows) // [{ id: 1, name: 'Alice' }]

await db.close()

Reopening an Existing Database

Use the same passphrase -- the salt is stored automatically.

const fs = new EncryptedFS(dataDir, 'my-secret-passphrase')
const db = await PGlite.create({ dataDir, fs })
// Your data is still there, decrypted transparently

If the passphrase is wrong, the constructor throws immediately with "Invalid passphrase or corrupted encryption keys".

pgvector Example

PGlite extensions work normally on top of the encrypted VFS. Here's pgvector:

import { PGlite } from '@electric-sql/pglite'
import { vector } from '@electric-sql/pglite/vector'
import { EncryptedFS } from 'pglite-encrypted-fs'

const dataDir = './my-encrypted-vectors'
const fs = new EncryptedFS(dataDir, process.env.DB_PASSPHRASE!)
const db = await PGlite.create({ dataDir, fs, extensions: { vector } })

await db.exec('CREATE EXTENSION IF NOT EXISTS vector')
await db.exec('CREATE TABLE docs (id serial PRIMARY KEY, embedding vector(3))')
await db.exec("INSERT INTO docs (embedding) VALUES ('[0.1, 0.2, 0.3]')")

const { rows } = await db.query(
  "SELECT * FROM docs ORDER BY embedding <-> '[0.1, 0.2, 0.3]' LIMIT 5"
)
console.log(rows)

await db.close()
fs.destroy()

API Reference

new EncryptedFS(dataDir, passphrase, options?)

Creates an encrypted filesystem instance.

| Parameter | Type | Description | |-----------|------|-------------| | dataDir | string | Path to the database directory on disk | | passphrase | string | Your encryption passphrase | | options | { debug?: boolean } | Optional. Enable debug logging with { debug: true } |

The constructor creates the data directory if it does not exist. On first use, it generates a random salt and creates a verification token. On subsequent opens, it reads the salt from the existing verification token and verifies the passphrase is correct.

deriveKeys(passphrase, salt)

Derives a 256-bit encryption key from a passphrase using PBKDF2-SHA512 with 256,000 iterations.

| Parameter | Type | Description | |-----------|------|-------------| | passphrase | string | The user's password or passphrase | | salt | Buffer | A 16-byte salt (from randomSalt() or stored) |

Returns { encKey: Buffer }. Takes approximately 48ms on modern hardware.

randomSalt()

Returns a 16-byte cryptographically random Buffer suitable for use with deriveKeys().

EncryptedFS.destroy()

Zeros the encryption key and salt from memory. Call this after closing PGlite to reduce the window of key exposure in heap dumps. Note that JavaScript's garbage collector may have already copied the data, so complete erasure is not guaranteed.

Constants

| Constant | Value | Description | |----------|-------|-------------| | PAGE_SIZE | 8192 | PostgreSQL page size (8KB) | | SALT_SIZE | 16 | Salt length in bytes | | FILE_HEADER_SIZE | 48 | File header: 16B salt + 32B file ID | | KDF_ITERATIONS | 256000 | PBKDF2 iteration count |

Security Design

Algorithm

AES-256-GCM with a random 12-byte IV generated per page write. The authentication tag (16 bytes) ensures both confidentiality and integrity.

Page Model

Each 8KB plaintext page becomes 8,220 bytes on disk:

[IV (12B)][Auth Tag (16B)][Ciphertext (8192B)]

File Layout

Every encrypted file on disk has this structure:

[Header (48B)][Encrypted Page 0][Encrypted Page 1][...]

Header = [Salt (16B)][File ID (32B)]

AAD (Additional Authenticated Data)

Each page's GCM authentication tag covers:

AAD = [File ID (32B)][Page Number (4B)]

This prevents two classes of attack:

  • Intra-file page swapping -- moving page 5 to page 3's slot within the same file is detected
  • Cross-file page swapping -- copying a page from one file into another file is detected

File IDs

Each file receives a random 32-byte identifier stored in its header. Because file IDs are not derived from the file path, encrypted files survive renames without breaking authentication.

Passphrase Verification

On first initialization, a .encryption-verify file is created containing the 16-byte salt followed by a known magic value encrypted with the derived key. On every subsequent open, the salt is read from this file, the key is re-derived, and the magic value is decrypted and checked. A wrong passphrase fails immediately rather than silently serving corrupted data.

Unencrypted Files

The following PostgreSQL metadata files are left unencrypted because they contain no user data and PostgreSQL requires them in plaintext:

  • .conf files (configuration)
  • .pid files (process ID)
  • PG_VERSION
  • pg_internal.init
  • postmaster.*
  • .lock files
  • replorigin_checkpoint

Threat Model

This provides at-rest encryption of PGlite/PostgreSQL database files on disk. It protects against offline theft or unauthorized access to the stored files.

Non-goals:

  • Does not protect against an attacker who can run code in your process
  • Data is decrypted in memory during query execution (like any database encryption-at-rest)
  • JavaScript runtimes cannot guarantee secure key erasure (destroy() is best-effort)
  • This package has not been independently audited. If you find a vulnerability, please report it privately via GitHub.

Performance

Benchmarks measured on Node.js (see pnpm run bench for your own results):

| Operation | Plain | Encrypted | Overhead | |-----------|-------|-----------|----------| | Insert 100 rows | 11.2ms | 13.6ms | +22% | | Bulk insert 1,000 rows | 5.5ms | 10.5ms | +93% | | Select 1,000 rows | 0.55ms | 0.54ms | ~0% | | Select with index | 0.090ms | 0.092ms | ~0% | | Aggregate (COUNT/SUM) | 0.100ms | 0.098ms | ~0% | | Mixed CRUD cycle | 0.42ms | 0.48ms | +15% | | Fresh database init | 531ms | 835ms | +57% | | Database reopen | 36ms | 86ms | +138% | | Single page encrypt | -- | 5.4us | -- | | Single page decrypt | -- | 3.9us | -- | | Key derivation (PBKDF2) | -- | 48ms | -- |

Read operations have near-zero overhead because data is decrypted when pages are loaded into PostgreSQL's buffer pool. Subsequent reads hit the cache, so reads are only slow the first time a page is loaded. Write overhead comes from per-page encryption. Run benchmarks yourself with pnpm run bench.

Platform Support

| Platform | Supported | |----------|-----------| | Node.js (>=20) | Yes | | Bun | Yes (untested) | | Deno | No | | Chrome | No | | Safari | No | | Firefox | No |

This package uses Node.js crypto and fs modules and is not compatible with browser environments.

FAQ

Can I rotate the passphrase?

Not in-place. Export your data with pg_dump (or application-level export), create a new encrypted database with the new passphrase, and re-import.

Can I migrate an existing plaintext PGlite database?

Same approach -- dump and re-import into a new encrypted database.

Why not SQLCipher?

SQLCipher encrypts SQLite databases. This package encrypts PGlite/PostgreSQL databases. If you're already using PGlite for its PostgreSQL features (extensions, SQL semantics, pgvector), this adds at-rest encryption without changing databases.

Contributing

git clone https://github.com/davidmuggleton/pglite-encrypted-fs.git
cd pglite-encrypted-fs
pnpm install
pnpm test
pnpm run bench

License

MIT -- see LICENSE.