Prisma +2 generator to emit a tRPC shield from your Prisma schema
52
stars
46
commits
TypeScript
primary language
Jul 28, 2026
updated
π‘οΈ Automatically generate tRPC Shield permissions from your Prisma schema
A powerful Prisma generator that creates tRPC Shield configurations from your Prisma schema. Automatically generates type-safe permission rules for all your database operations, saving you time and reducing boilerplate code.
If this tool helps you build better applications, please consider supporting its development:
Your sponsorship helps maintain and improve this project. Thank you! π
Now with full Prisma 7 & tRPC 11 support!
npm install prisma-trpc-shield-generator
This release builds on Prisma 7 and tRPC v11. The generator depends on @prisma/generator-helper and @prisma/internals at ^7.0.0, and the shield it emits is typed against trpc-shield v2, which itself requires @trpc/server v11. Report any issues to help us continue improving!
| Package | Version |
|---|---|
| Prisma | 7.x |
trpc-shield | 2.x |
@trpc/server | 11.x |
| Node.js | 22 and 24 are the versions CI runs against |
prisma generatenpm install prisma-trpc-shield-generator trpc-shield
schema.prisma:// The generator reads your models through a Prisma Client generator, so your schema needs one.
// On Prisma 7 that is `prisma-client`; on Prisma 5 and 6 it was `prisma-client-js`.
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
generator trpc_shield {
provider = "prisma-trpc-shield-generator"
contextPath = "../src/context"
}
datasource db {
provider = "sqlite"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
npx prisma generate
import { permissions } from './generated/shield';
import { t } from './trpc';
export const permissionsMiddleware = t.middleware(permissions);
export const protectedProcedure = t.procedure.use(permissionsMiddleware);
The generator creates a comprehensive shield configuration with all CRUD operations:
import { shield, allow } from 'trpc-shield';
import { Context } from '../../../context';
export const permissions = shield<Context>({
query: {
// Find operations
findUniqueUser: allow,
findFirstUser: allow,
findManyUser: allow,
findUniquePost: allow,
findFirstPost: allow,
findManyPost: allow,
// Aggregation operations
aggregateUser: allow,
aggregatePost: allow,
groupByUser: allow,
groupByPost: allow,
},
mutation: {
// Create operations
createOneUser: allow,
createOnePost: allow,
// Update operations
updateOneUser: allow,
updateOnePost: allow,
updateManyUser: allow,
updateManyPost: allow,
// Delete operations
deleteOneUser: allow,
deleteOnePost: allow,
deleteManyUser: allow,
deleteManyPost: allow,
// Upsert operations
upsertOneUser: allow,
upsertOnePost: allow,
},
});
| Option | Description | Type | Default |
|---|---|---|---|
output | Output directory for the generated shield | string | ./generated |
contextPath | Path to your tRPC context file, relative to the directory holding your schema.prisma | string | ../../../../src/context |
contextPath is resolved against the schema's directory, not against output. The generator then
rewrites it as a path relative to output in the emitted import, so moving output does not
require changing contextPath.
generator trpc_shield {
provider = "prisma-trpc-shield-generator"
output = "./src/shields"
contextPath = "../context"
}
Replace the default allow rules with your custom logic:
import { permissions } from './generated/shield';
import { rule, and, or } from 'trpc-shield';
const isAuthenticated = rule()(async (parent, args, ctx) => {
return ctx.user !== null;
});
const isOwner = rule()(async (parent, args, ctx) => {
const post = await ctx.prisma.post.findUnique({
where: { id: args.where.id },
select: { authorId: true }
});
return post?.authorId === ctx.user?.id;
});
// Override specific permissions
export const customPermissions = {
...permissions,
mutation: {
...permissions.mutation,
createOnePost: and(isAuthenticated),
updateOnePost: and(isAuthenticated, isOwner),
deleteOnePost: and(isAuthenticated, isOwner),
}
};
import { initTRPC } from '@trpc/server';
import { customPermissions } from './shields/permissions';
const t = initTRPC.context<Context>().create();
export const permissionsMiddleware = t.middleware(customPermissions);
export const protectedProcedure = t.procedure.use(permissionsMiddleware);
export const appRouter = t.router({
user: t.router({
create: protectedProcedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input, ctx }) => {
return ctx.prisma.user.create({ data: input });
}),
}),
});
import { rule, and } from 'trpc-shield';
const isAuthenticated = rule()(async (parent, args, ctx) => {
return !!ctx.user;
});
const canManagePosts = rule()(async (parent, args, ctx) => {
if (!ctx.user) return false;
// Admin can manage all posts
if (ctx.user.role === 'ADMIN') return true;
// Users can only manage their own posts
if (args.where?.authorId) {
return args.where.authorId === ctx.user.id;
}
return false;
});
export const permissions = shield<Context>({
query: {
findManyPost: allow, // Public read access
findUniquePost: allow,
findManyUser: isAuthenticated, // Authenticated read access
},
mutation: {
createOnePost: isAuthenticated,
updateOnePost: and(isAuthenticated, canManagePosts),
deleteOnePost: and(isAuthenticated, canManagePosts),
},
});
Error: Cannot find module '../context'
contextPath is correct relative to the output directoryContext typeTypeScript errors in generated shield
trpc-shield is installed and up to dateShield not updating after schema changes
npx prisma generate after modifying your schemaschema.prismaContributions are welcome! Please read our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ by Omar Dulaimi
39 commits
7 commits
TypeScript
77.4%
JavaScript
19.7%
Shell
3.0%
Prisma +2 generator to emit a tRPC shield from your Prisma schema
52
stars
46
commits
TypeScript
primary language
Jul 28, 2026
updated
π‘οΈ Automatically generate tRPC Shield permissions from your Prisma schema
A powerful Prisma generator that creates tRPC Shield configurations from your Prisma schema. Automatically generates type-safe permission rules for all your database operations, saving you time and reducing boilerplate code.
If this tool helps you build better applications, please consider supporting its development:
Your sponsorship helps maintain and improve this project. Thank you! π
Now with full Prisma 7 & tRPC 11 support!
npm install prisma-trpc-shield-generator
This release builds on Prisma 7 and tRPC v11. The generator depends on @prisma/generator-helper and @prisma/internals at ^7.0.0, and the shield it emits is typed against trpc-shield v2, which itself requires @trpc/server v11. Report any issues to help us continue improving!
| Package | Version |
|---|---|
| Prisma | 7.x |
trpc-shield | 2.x |
@trpc/server | 11.x |
| Node.js | 22 and 24 are the versions CI runs against |
prisma generatenpm install prisma-trpc-shield-generator trpc-shield
schema.prisma:// The generator reads your models through a Prisma Client generator, so your schema needs one.
// On Prisma 7 that is `prisma-client`; on Prisma 5 and 6 it was `prisma-client-js`.
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
generator trpc_shield {
provider = "prisma-trpc-shield-generator"
contextPath = "../src/context"
}
datasource db {
provider = "sqlite"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
npx prisma generate
import { permissions } from './generated/shield';
import { t } from './trpc';
export const permissionsMiddleware = t.middleware(permissions);
export const protectedProcedure = t.procedure.use(permissionsMiddleware);
The generator creates a comprehensive shield configuration with all CRUD operations:
import { shield, allow } from 'trpc-shield';
import { Context } from '../../../context';
export const permissions = shield<Context>({
query: {
// Find operations
findUniqueUser: allow,
findFirstUser: allow,
findManyUser: allow,
findUniquePost: allow,
findFirstPost: allow,
findManyPost: allow,
// Aggregation operations
aggregateUser: allow,
aggregatePost: allow,
groupByUser: allow,
groupByPost: allow,
},
mutation: {
// Create operations
createOneUser: allow,
createOnePost: allow,
// Update operations
updateOneUser: allow,
updateOnePost: allow,
updateManyUser: allow,
updateManyPost: allow,
// Delete operations
deleteOneUser: allow,
deleteOnePost: allow,
deleteManyUser: allow,
deleteManyPost: allow,
// Upsert operations
upsertOneUser: allow,
upsertOnePost: allow,
},
});
| Option | Description | Type | Default |
|---|---|---|---|
output | Output directory for the generated shield | string | ./generated |
contextPath | Path to your tRPC context file, relative to the directory holding your schema.prisma | string | ../../../../src/context |
contextPath is resolved against the schema's directory, not against output. The generator then
rewrites it as a path relative to output in the emitted import, so moving output does not
require changing contextPath.
generator trpc_shield {
provider = "prisma-trpc-shield-generator"
output = "./src/shields"
contextPath = "../context"
}
Replace the default allow rules with your custom logic:
import { permissions } from './generated/shield';
import { rule, and, or } from 'trpc-shield';
const isAuthenticated = rule()(async (parent, args, ctx) => {
return ctx.user !== null;
});
const isOwner = rule()(async (parent, args, ctx) => {
const post = await ctx.prisma.post.findUnique({
where: { id: args.where.id },
select: { authorId: true }
});
return post?.authorId === ctx.user?.id;
});
// Override specific permissions
export const customPermissions = {
...permissions,
mutation: {
...permissions.mutation,
createOnePost: and(isAuthenticated),
updateOnePost: and(isAuthenticated, isOwner),
deleteOnePost: and(isAuthenticated, isOwner),
}
};
import { initTRPC } from '@trpc/server';
import { customPermissions } from './shields/permissions';
const t = initTRPC.context<Context>().create();
export const permissionsMiddleware = t.middleware(customPermissions);
export const protectedProcedure = t.procedure.use(permissionsMiddleware);
export const appRouter = t.router({
user: t.router({
create: protectedProcedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input, ctx }) => {
return ctx.prisma.user.create({ data: input });
}),
}),
});
import { rule, and } from 'trpc-shield';
const isAuthenticated = rule()(async (parent, args, ctx) => {
return !!ctx.user;
});
const canManagePosts = rule()(async (parent, args, ctx) => {
if (!ctx.user) return false;
// Admin can manage all posts
if (ctx.user.role === 'ADMIN') return true;
// Users can only manage their own posts
if (args.where?.authorId) {
return args.where.authorId === ctx.user.id;
}
return false;
});
export const permissions = shield<Context>({
query: {
findManyPost: allow, // Public read access
findUniquePost: allow,
findManyUser: isAuthenticated, // Authenticated read access
},
mutation: {
createOnePost: isAuthenticated,
updateOnePost: and(isAuthenticated, canManagePosts),
deleteOnePost: and(isAuthenticated, canManagePosts),
},
});
Error: Cannot find module '../context'
contextPath is correct relative to the output directoryContext typeTypeScript errors in generated shield
trpc-shield is installed and up to dateShield not updating after schema changes
npx prisma generate after modifying your schemaschema.prismaContributions are welcome! Please read our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ by Omar Dulaimi
39 commits
7 commits
TypeScript
77.4%
JavaScript
19.7%
Shell
3.0%