@playpals/db
packages/database
The Prisma schema, the generated client, and a set of query helpers. Every backend service imports this package; there is no per-service database access.
What it exports
packages/database/src/index.ts:
export { prisma } from "./client";
export * from "./error";
export * from "./queries";
export * from "../generated/prisma"; // model types, enums, Prisma namespaceSo a service gets the client, the helpers, the error types, and every generated model type from one import:
import { prisma, getUserById, NotFoundError, type User, NotificationType } from "@playpals/db";The client
src/client.ts builds a pg pool, wraps it in the Prisma driver adapter, and memoises the client on globalThis outside production so hot reload does not exhaust connections.
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;In production the connection string is parsed manually rather than passed through:
const url = new URL(dbUrl);
return new Pool({
host: url.hostname,
port: url.port ? Number(url.port) : 5432,
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database: url.pathname.slice(1),
ssl: { rejectUnauthorized: false },
});The comment in the source explains why: DigitalOcean managed Postgres presents a self-signed CA, and parsing the URL by hand stops a ?sslmode= parameter in the connection string from overriding the ssl config. rejectUnauthorized: false is the deliberate consequence — the connection is encrypted, but the certificate is not verified. Supplying DigitalOcean's CA certificate and setting rejectUnauthorized: true would be strictly better.
Query helpers
src/queries/ holds hand-written helpers for the most common access patterns, one file per domain:
comment.ts feed.ts follow.ts game.ts post.ts
product.ts reaction.ts report.ts trending.ts
pushNotificationTokens.ts user.ts verificationAsset.tsThese are a convenience, not a repository layer — services also use prisma directly. Put a helper here when the same query appears in more than one service; otherwise keep it in the service that needs it.
Errors
export class NotFoundError extends Error {
constructor(message?: string) {
super(message ?? "Not found");
this.name = "NotFoundError";
}
}Scripts
cd packages/database
pnpm db:generate # prisma generate → generated/prisma
pnpm db:migrate # prisma migrate dev — creates and applies a migration
pnpm db:deploy # prisma migrate deploy — production, applies only
pnpm db:push # push schema without a migration — local experiments only
pnpm db:seed # prisma/seed.ts
pnpm build # tsc, then copy generated/ into dist/ and fix its importsThe build script does more than compile. copy:generated copies the generated Prisma client into dist/generated, and fix:generated-imports rewrites its import paths so the published dist/ is self-contained. That is why pnpm build is required after a schema change, not just prisma generate.
Configuration
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL connection string. Required — the client throws at startup without it. |
Gotchas
generated/is git-ignored. A fresh clone will not typecheck untilpnpm db:generateruns.- Rebuild after a schema change. Services import from
dist/, sopnpm build --filter=@playpals/dbis what makes new model types visible to them. - The datasource has no
urlin the schema.schema.prismadeclares onlyprovider = "postgresql"; the connection comes from the driver adapter inclient.ts.
See Database Overview and Migrations & Seeding.
