Quickstart

This guide takes you from an empty file to an agent answering a question over your database. You connect valv, write one policy, hand the tools to a model, and let it run a query. The example uses ClickHouse and the Vercel AI SDK; the shape is identical for Prisma.

Before you start, install an adapter for your database.

Using a coding agent? To point a tool like Claude Code at a database with no code, go straight to MCP server instead.

1. Connect

createValv loads your schema on construction, so the instance is ready to use. Call it once at startup.

import { createValv } from "@valv/clickhouse"

const valv = await createValv(client, {
  schema: "introspect", // read the live schema
  database: "analytics",
  defaultPolicy: "deny-all", // every resource is hidden until you allow it
})

defaultPolicy: "deny-all" is the recommended setting: a resource stays invisible to the model until you write a policy for it.

2. Write a policy

A policy is a function of your request context. It decides what the caller may read, per resource. Here every read of orders is scoped to the caller’s tenant:

valv.policy("orders", (ctx) => ({
  read: { tenant_id: ctx.tenant.id },
}))

The model can’t see, widen, or remove this filter. valv injects it into the WHERE clause on the server. See Policies for row filters, hidden columns, and write rules.

3. Hand the tools to your agent

valv.tools.aisdk(ctx) returns self-executing tools bound to the current context. Pass them to the model along with the user’s question:

import { generateText, stepCountIs } from "ai"

const ctx = { user: { id: "u1", role: "analyst" }, tenant: { id: "acme" } }

const { text } = await generateText({
  model,
  tools: await valv.tools.aisdk(ctx),
  stopWhen: stepCountIs(6),
  prompt: "What's our revenue per order status this month?",
})

The model gets four tools: list_resources, search_resources, describe_resource, and query. It discovers your schema, composes a query, and valv scopes it to acme, compiles it to ClickHouse SQL, runs it, and hands back rows.

Using Prisma instead

The only change is the import and connection. Prisma reads the schema from your generated client, so you don’t pass schema or database:

import { createValv } from "@valv/prisma"

const valv = await createValv(prisma, { defaultPolicy: "deny-all" })

Next steps