datewise/app/db.server.ts
metatroncubeswdev 1d2d943d54 feat(security): encrypt customer personal data at rest
- Prisma extension seals booking name/email/phone/address and waitlist contacts with
  AES-256-GCM on write and opens them on read (incl. nested reads)
- keyed-hash columns for exact email/phone lookups (privacy webhooks, booking search,
  waitlist de-duplication); migration 0002
- tests prove only ciphertext is stored; privacy/terms/SECURITY.md updated to match

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:56:05 -04:00

36 lines
1.2 KiB
TypeScript

import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { piiExtension } from "./lib/pii.server";
declare global {
// eslint-disable-next-line no-var
var prismaGlobal: PrismaClient | undefined;
}
/**
* Rust-free Prisma client over node-postgres (no native engine binary needed,
* which also makes shared/VPS hosting simpler). Every client encrypts customer
* personal data at rest (lib/pii.server.ts). The extension keeps the field types,
* so the client is typed as a plain PrismaClient.
*/
export function createPrisma(url = process.env.DATABASE_URL): PrismaClient {
if (!url) throw new Error("DATABASE_URL is not set");
return new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) }).$extends(piiExtension) as unknown as PrismaClient;
}
function instance(): PrismaClient {
if (!global.prismaGlobal) global.prismaGlobal = createPrisma();
return global.prismaGlobal;
}
/** Lazily created so importing this module never needs DATABASE_URL (tests, build). */
const prisma = new Proxy({} as PrismaClient, {
get(_t, prop) {
const db = instance();
const v = Reflect.get(db, prop);
return typeof v === "function" ? v.bind(db) : v;
},
});
export default prisma;