Skip to content

@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:

ts
export { prisma } from "./client";
export * from "./error";
export * from "./queries";
export * from "../generated/prisma";     // model types, enums, Prisma namespace

So a service gets the client, the helpers, the error types, and every generated model type from one import:

ts
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.

ts
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:

ts
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.ts

These 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

ts
export class NotFoundError extends Error {
  constructor(message?: string) {
    super(message ?? "Not found");
    this.name = "NotFoundError";
  }
}

Scripts

bash
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 imports

The 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

VariablePurpose
DATABASE_URLPostgreSQL connection string. Required — the client throws at startup without it.

Gotchas

  • generated/ is git-ignored. A fresh clone will not typecheck until pnpm db:generate runs.
  • Rebuild after a schema change. Services import from dist/, so pnpm build --filter=@playpals/db is what makes new model types visible to them.
  • The datasource has no url in the schema. schema.prisma declares only provider = "postgresql"; the connection comes from the driver adapter in client.ts.

See Database Overview and Migrations & Seeding.

Internal documentation — PlayPalz platform