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

@vistal/prisma

v0.3.0

Published

Prisma adapter for vistal — row-level security and access control for AI agents

Downloads

1,010

Readme

@vistal/prisma

Row-level security and access control for AI agents — Prisma adapter.

npm license TypeScript

Reads your Prisma schema, generates typed LLM tools, and enforces row-level security and field-level access control server-side on every query — in code, not prompts.


Installation

npm install @vistal/prisma @vistal/core
npm install --save-dev prisma

Requires Prisma 5+ and @prisma/client as peer dependencies.


Setup

import { PrismaClient } from "@prisma/client"
import { createVistal } from "@vistal/prisma"

const prisma = new PrismaClient()

const vistal = createVistal(prisma, { defaultPolicy: "deny-all" })

createVistal infers resource names from your Prisma client type — policy keys are type-checked, so a typo is a compile error. Prisma model names are converted to snake_case resource names (OrderItemorder_item).

If your schema isn't at ./prisma/schema.prisma, pass schemaPath:

const vistal = createVistal(prisma, {
  defaultPolicy: "deny-all",
  schemaPath: "./db/schema.prisma",
})

Schema annotations

Use /// doc comments to give the LLM better context and mark fields that should never leave the server:

/// @vistal:description "A customer purchase order"
model Order {
  id        String      @id @default(uuid())
  tenant_id String
  status    OrderStatus
  total     Decimal

  /// @vistal:description "Order total in cents"
  total     Decimal

  /// @vistal:sensitive
  internal_notes String?
}

| Annotation | Effect | |---|---| | @vistal:description "..." | Added to the tool description so the LLM understands the resource or field | | @vistal:sensitive | Field is stripped at introspection — never appears in tool schemas, arguments, or results |

@vistal:sensitive is enforced before policy runs. The field doesn't exist as far as the LLM is concerned.


Policies

vistal.policy("order", (ctx) => ({
  read:   { tenant_id: ctx.tenant.id },   // row filter — AND-ed into every read
  write:  { tenant_id: ctx.tenant.id },   // force-injected on INSERT, AND-ed into UPDATE WHERE
  delete: false,                           // delete_order tool never generated
  fields:    { deny: ctx.user.role === "support" ? ["internal_notes"] : [] },
  relations: { items: true, customer: ctx.user.role === "admin" },
}))

// Wildcard fallback for resources without an explicit policy()
vistal.policy("*", (ctx) => ({
  read:   { tenant_id: ctx.tenant.id },
  write:  false,
  delete: false,
}))

read, write, and delete accept:

| Value | Meaning | |---|---| | true | allow | | false | deny — no tool generated for this operation | | { field: value } | row filter (read/delete) or force-injected field (write) |


Connecting to your LLM provider

// Vercel AI SDK (requires the `ai` package)
const tools = await vistal.tools.vercel(ctx)
const { text } = await generateText({ model, tools, maxSteps: 8, prompt })

// Anthropic
const tools = await vistal.tools.anthropic(ctx)
await anthropic.messages.create({ tools: tools.map(t => t.definition) })
const result = await tools.find(t => t.name === block.name)!.execute(block.input)

// OpenAI
const tools = await vistal.tools.openai(ctx)

// Gemini
const tools = await vistal.tools.gemini(ctx)

How Prisma model names map to resource names

Prisma model names (PascalCase) are converted to snake_case resource names:

| Prisma model | vistal resource | |---|---| | Order | order | | OrderItem | order_item | | UserProfile | user_profile |

These are the strings you pass to policy() and that appear in generated tool names (query_order_item, create_order_item, …).


Security properties

| Property | How Prisma enforces it | |---|---| | Row filters on read | where clause passed to findMany / findFirst | | Write scoping | write: { tenant_id } is injected into create data and AND-ed into updateMany / deleteMany WHERE — cross-tenant records won't match | | update / delete use Many | Ensures the full policy where (id + forced filter) is applied — a mismatched tenant gets { count: 0 }, not an error that leaks existence | | belongsTo relation filters | Enforced post-fetch in memory (Prisma doesn't support where on to-one includes) | | Sensitive fields | Stripped at introspection via @vistal:sensitive — never passed to Prisma in select, data, or returned in results |


Live views with LISTEN/NOTIFY

Live views poll by default. On Postgres you can replace polling with native change notifications (requires the optional peer dependency pg):

import { createVistal, installLiveTriggers } from "@vistal/prisma"

// Once per database — e.g. in a migration step. Table names are the actual
// Postgres table names (the Prisma model name unless @@map is used).
await installLiveTriggers(prisma, ["Order", "User"])

const vistal = createVistal(prisma, {
  live: {
    connectionString: process.env.DATABASE_URL!,
    channel: "vistal_changes",   // default
    debounceMs: 250,             // coalesce notification bursts (default)
    onError: (e) => logger.warn("live updates unavailable", e),
  },
})

How it works: statement-level triggers pg_notify the table name only on one channel; a single dedicated pg connection LISTENs and routes notifications to the views watching that table (including tables of eager-loaded relations). On a notification the view re-executes through the full policy pipeline — notifications never carry data, so they can never bypass policy. The LISTEN connection starts lazily with the first subscription and closes with the last. If the connection can't be established (e.g. pg missing), onError fires and affected views go stale — monitor it, or omit live to stay on polling.

liveTriggersSQL(tables, channel?) returns the raw SQL if you prefer to manage triggers in your own migrations.


Exports

| Export | Purpose | |---|---| | createVistal(prisma, config?) | Main entry point — creates a Vistal instance with the Prisma adapter wired up and resource types inferred | | PrismaAdapter | The adapter class if you need to instantiate it separately | | translateFilter | Converts a vistal FilterNode to a Prisma where object — useful when building a custom adapter on top of Prisma | | installLiveTriggers / liveTriggersSQL | Install (or emit) the LISTEN/NOTIFY triggers for live views | | PgNotifyListener | The LISTEN connection manager, if you need to wire it manually |


Other adapters

For ClickHouse, use @vistal/clickhouse instead — same policy API, no relations, schema annotations via column COMMENTs.


License

MIT