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

@btravstack/prisma

v0.58.0

Published

The Prisma starter for @btravstack/core: DATABASE_URL through Config, the Postgres driver adapter, and a client whose pool is the application scope's

Readme

@btravstack/prisma

The Prisma starter for @btravstack/core: DATABASE_URL bound through Config, per-query observation as middleware, and a client whose pool is the application scope's. Prisma 8.

📖 Documentation · API Reference

pnpm add @btravstack/prisma @btravstack/core @btravstack/config @btravstack/di unthrown \
  @prisma/[email protected] [email protected]

Install the required peers, so the application holds a single copy of each; temporal-polyfill is the Postgres package's own peer from rc.14. The @prisma/orm-postgres peer is ^8.0.0-rc.11, and both ends are tested: an application already on rc.11 keeps it.

Upgrading with released migrations: upgrade @btravstack/prisma and its @btravstack/* peers, and leave Prisma where it is. No contract, migration or database marker changes. Upgrading Prisma itself from rc.11 rewrites every released migration and re-signs every database, which is Prisma's path rather than this package's; see Supported Prisma trains. The prisma CLI is yours, as a dev dependency: the client is built from your emitted contract, so there is none for this package to ship. Node >=22.12.

Production packaging: the pinned Postgres facade requires @prisma/orm-toolchain, including its CLI-engine peer and build-tool closure. A /runtime import and pnpm deploy --prod do not remove those transitive dependencies. Keep contract emission in the build stage and migrations in a deployment Job, with strict peers enabled; a toolchain-free runtime install remains blocked upstream. That section documents the manifest split and the tracked dependency.

This is Prisma 8, which is a different package family rather than a version bump — @prisma/orm-postgres replaces @prisma/client and @prisma/adapter-pg both, and there is no @prisma/instrumentation. For Prisma 7 use @btravstack/[email protected].

A worked example

import { prismaDatabase } from "@btravstack/prisma";
import { contractFromJson } from "@prisma/orm-postgres/static";

const contract = contractFromJson<Contract>(contractJson);
const database = prismaDatabase("OrderDatabase")({
  client: ({ url, middleware }) => postgres({ contract, url, middleware }),
});

That is the whole surface. database is a module carrying the port a composition root reads; the provider binding DATABASE_URL, the resourceful client provider and the health check are inside it:

export const PersistenceModule = Module("Persistence")({
  imports: [database],
  exports: [database.port],
  needs: [Env],
});

The client type is yours, and that is deliberate. A Prisma 8 client is typed by your emitted Contract and built from your contract.json, so there is none for this package to ship. From Prisma rc.19, turn that JSON into a typed value once with contractFromJson<Contract>(contractJson) and pass contract to postgres({ contract, ... }). The client arrow is where your contract and this starter meet, and the port is typed by exactly what it returns. Spread the middleware it hands you into the client: that is where the per-query observation below comes from.

What it owns

  • DATABASE_URL through Config. A missing or blank value is a modeled ConfigInvalid naming the variable, not a throw — so a misconfigured deployment exits 78 with the reason on stderr instead of crashing on the first query.
  • The pool's lifetime. The provider is resourceful, so release closes it on every exit path, including a boot that fails after it ran.
  • A database that answers before any work runs. Opening the scope runs the health check's probe, so a fresh pool is verified before its first burst of transactions, and a database that does not answer fails the boot with DatabaseUnreachable — the database's name and the driver's cause, never the URL.
  • A role row security applies to. Where any table has row-level security enabled, a role that is a superuser, has BYPASSRLS, or owns a row-secured table that is not FORCEd fails the boot with RowSecurityBypassed, naming the role and the tables it would bypass — every policy would admit every row for it.
  • A health check, named after the starter — SELECT 1 through the raw lane, folded into the kernel's GET /healthz with nothing wired. /readyz does not read it: failing readiness on a dependency every replica shares removes them all at once.
  • Every query handed to Observers, as one statement middleware, so composing observability() writes the failures as lines and otel() opens the spans and mints the instruments — with no flag here and no port list to satisfy when you compose neither. A middleware sees the ORM lane, the SQL builder and the raw lane alike, and carries the runtime's own latencyMs.

Results, on the @btravstack/prisma/result subpath

Prisma 8 throws a structured error carrying PostgreSQL's own SQLSTATE. tryQuery turns that into a Result with three modeled arms — UniqueConstraintViolation (23505, with the constraint the database named), ForeignKeyViolation (23503) and NotAuthorized (42501, including a write a row-security policy refused). Anything else is a defect, unless you list it in raises — a SQLSTATE your own trigger raises on purpose then arrives as a RaisedException carrying that code.

const saved = tryQuery(() => db.orm.orders.Order.create({ orderId })).mapErrCases(
  (matcher, defect) =>
    matcher
      .with(P.tag("UniqueConstraintViolation"), () => duplicate(orderId))
      .with(P.tag("ForeignKeyViolation"), P.tag("NotAuthorized"), (cause) => defect(cause)),
);

It takes a thunk rather than a promise: an AsyncResult is eager, so a promise built at the call site has already started before the Result exists.

Row-level security, on the @btravstack/prisma/rls subpath

The policy is declared in your contract — @@rls on the model and a policy_all block beside it — and the planner emits ENABLE ROW LEVEL SECURITY and CREATE POLICY like any other migration operation. The pin is a transaction:

const orders = await tenantPinned(db, tenant, (tx) => tx.orm.orders.Order.all());

For a consistent snapshot, pass { isolationLevel: "repeatable read", readOnly: true } as the fourth argument. The starter establishes these characteristics before the tenant pin. Use its boot-probed client and the callback's tx for every related read; omitted characteristics preserve the connection defaults. See consistent snapshots.

set_config(…, true) is transaction-local, which is why this opens one rather than handing back a pinned client: a session-scoped pin works only while the pool happens to return the same connection, and fails silently the first time it does not. The policy must read the same setting — app.tenant_id is only the default — because a tenantPinned(db, tenant, work, { setting }) and a policy naming a different one deny every row and every write, which looks exactly like row security working.

Two halves stay the deployment's, and both fail quietly when forgotten: Prisma 8 authors policies but no GRANTs, and it emits ENABLE ROW LEVEL SECURITY with no way to say FORCE — so the table's owner, which is the role that ran the migrations, bypasses every policy. Connect as a role that is neither the owner nor a superuser — the boot refuses either with RowSecurityBypassed. The reference page carries the DDL and the smaller lines that fail the same way.

Tenant discovery for scheduled work

tenantListing on /rls declares a tenant-ID-only query (100 IDs per page, resuming after the last ID) and a checked Prisma migration installing its function, owner policy and execution grant. It works under forced row-level security while ordinary application reads remain tenant-scoped. Shared data does not require a privileged application connection. See tenant listing.

What it does not

Running migrations. A deployment runs prisma db migrate against the same database before the process starts — and, where row security is on, as its owner rather than the role DATABASE_URL carries. An application that migrates itself at boot races every other replica.

Engine-level tracing. There is no engine: Prisma 8 is a TypeScript runtime and ships no instrumentation package, so the statement middleware is the whole seam and it already carries the runtime's own latency.

Transactions. Commit boundaries belong to the adapter, spelled at the call — db.transaction(fn) is the primitive. The rls subpath opens one transaction to pin a setting on and the work inside it is yours. There is no unit-scoped transaction and there will not be one; see the kernel maps nothing.

A repository base class. Ports are your application's vocabulary, not this package's.

Options

| Option | Where | What it is | | --------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | name | prismaDatabase(name) | the port's id and the health check's name — required | | client | prismaDatabase(name)({ client }) | builds your client from the URL and middleware this package bound — required | | DATABASE_URL | environment, read by prismaDatabase(name) | the connection string — required, validated at graph build; blank is an error, exit 78 | | setting | tenantPinned(db, tenant, work, { setting }) | the PostgreSQL run-time setting the policy reads — default app.tenant_id | | isolationLevel, readOnly | tenantPinned(db, tenant, work, options) | transaction characteristics established before the pin — connection defaults when omitted | | raises | tryQuery(run, { raises }), tenantPinned(…, { raises }) | SQLSTATEs modeled as RaisedException rather than defects — default none | | schema, table, tenantColumn | tenantListing(options) | the physical table and tenant column for a narrow listing | | name, owner, role | tenantListing(options) | the listing function, its trusted owner and application caller |

There is no instrumented flag: observation is a set port every call is handed to, so a graph composing no observability pays one inert call and no port list. The full table — defaults, semantics and the reasoning — lives on the reference page, which is this list's one detailed home.

License

MIT