Production-grade data protection primitives for Node and Next.js apps, pulled from a real app and made standalone:
- Field-level encryption — AES-256-GCM with per-message IVs and owner-bound authentication, for protecting sensitive columns at rest.
- PII-safe error reporting — a Sentry
beforeSendhook that strips emails, tokens, request bodies, and captured locals before an error ever leaves the machine.
Two things most apps get slightly wrong, done carefully and with the trade-offs written down. Zero runtime dependencies, zero npm audit findings — the only dev dependency is TypeScript; tests run on Node's built-in test runner.
npm install # dev deps for building + tests
npm test
npm run buildPublished as a reference implementation — copy the file you need, or build and import the package. It's small enough to read end to end in five minutes.
Encrypt a single sensitive value before it touches the database.
import { encryptField, decryptField } from "nextjs-privacy-kit";
// Generate a key once: openssl rand -hex 32
// Store it server-side as FIELD_ENCRYPTION_KEY.
const { data, version } = encryptField("my private note", user.id);
// -> persist `data` (bytea/Buffer) and `version` (int) on the row
const plaintext = decryptField(row.data, user.id, row.version);| Decision | Reason |
|---|---|
| AES-256-GCM | Authenticated encryption — confidentiality and integrity. Tampering is detected on decrypt, it doesn't silently return garbage. |
| Fresh 12-byte IV per message | GCM is catastrophically broken if an IV is reused under the same key. A random IV per call avoids it; the IV is prepended to the ciphertext (it's not secret). |
| AAD = owner id | The owner id is authenticated but not encrypted. Copy one user's ciphertext into another user's row and decryption fails — ciphertext substitution is caught, not accepted. |
| Versioned format | A version int travels with every blob, so the scheme can evolve. Key rotation is an explicit re-encryption migration — you never swap the key in place and brick old rows. |
| Stored layout | IV (12) ‖ ciphertext ‖ authTag (16) in one column. Self-contained, no extra columns to keep in sync. |
Kerckhoffs's principle: the security rests entirely on
FIELD_ENCRYPTION_KEY, not on this code being secret — which is exactly why it can be open source.
Defense in depth for your observability pipeline. scrubEvent is generic over the event type and has no dependency on the Sentry SDK, so it drops straight into beforeSend while keeping Sentry's own ErrorEvent type at the call site:
import * as Sentry from "@sentry/nextjs";
import { scrubEvent } from "nextjs-privacy-kit";
Sentry.init({
dsn: process.env.SENTRY_DSN,
beforeSend: scrubEvent,
});It will:
- Redact emails and
Bearer/JWT-shaped tokens inside any string. - Blank values whose key looks sensitive (
password,token,authorization,api_key, …). ExtendSENSITIVE_KEYSfor your domain. - Drop request cookies, body (
data), and auth headers entirely. - Scrub captured stack-frame locals (
vars). - Remove
user.email,user.username,user.ip_address— an anonymoususer.idis kept so you can still group events.
import { SENSITIVE_KEYS } from "nextjs-privacy-kit";
// e.g. add your own: ["...SENSITIVE_KEYS", "note", "message"]This is the last line of defense, not the only one — the goal is that a stray PII attach upstream degrades to [email-filtered] instead of a breach.
Run on Node's built-in test runner (node:test) — no test framework dependency. The suite covers the parts that are easy to get subtly wrong:
- encryption: round-trip, unicode, unique IVs, tamper detection (auth tag), wrong-AAD rejection, version mismatch, malformed payload, key validation.
- scrubbing: email/token redaction, sensitive-key blanking, recursion + cycle safety, request/user stripping.
npm test # compiles with tsc, then runs node --testMIT — see LICENSE.