ZenStack plugin for generating oRPC routers with automatic Row Level Security support
npm install zenstack-orpcZenStack plugin for generating oRPC routers with automatic Row Level Security support.
- ā
Generate oRPC routers directly from ZenStack schema
- ā
Full Row Level Security (RLS) policy support
- ā
Automatic validation via Zod schemas
- ā
Type-safe API with complete type inference
- ā
Access error handling (P2004)
- ā
Support for all Prisma CRUD operations
- ā
Simplified architecture (no adapter layer needed)
``bash`
npm install -D zenstack-orpcor
pnpm add -D zenstack-orpcor
yarn add -D zenstack-orpc
`zmodel`
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc'
}
`bash`
npx zenstack generate
`typescript
import { appRouter } from './zenstack/orpc/routers';
import { createORPCHandler } from '@orpc/server';
import { PrismaClient } from '@prisma/client';
const handler = createORPCHandler({
router: appRouter,
context: async (req) => ({
prisma: new PrismaClient(),
user: await getUserFromRequest(req),
}),
});
`
``
zenstack/orpc/
āāā routers/
ā āāā index.ts # Main router (appRouter)
ā āāā User.router.ts
ā āāā Post.router.ts
ā āāā ... # Router for each model
āāā helper.ts # Utilities and context
`zmodel`
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc' // Output path
generateModels = ['User', 'Post'] // Generate only specified models
generateModelActions = ['findMany', 'create'] // Generate only specified operations
zodSchemasImport = '../../zod' // Path to Zod schemas import
baseImport = '../base' // Import base context from custom path
}
By default, the plugin generates a basic context. You can provide your own:
`typescript
// base.ts
import { os } from '@orpc/server';
import type { PrismaClient } from '@prisma/client';
export const base = os.$context<{
prisma: PrismaClient;
user?: {
id: string;
role: string;
// Add your custom fields
};
}>();
`
Then use baseImport option:
`zmodel`
plugin orpc {
provider = 'zenstack-orpc'
output = 'zenstack/orpc'
baseImport = '../base'
}
The plugin generates a typed context:
`typescript`
type Context = {
prisma: PrismaClient;
user?: {
id: string;
role: string;
};
};
For each model, the following operations are generated:
Queries (8 operations):
- aggregate - aggregate datacount
- - count recordsfindFirst
- - find first recordfindFirstOrThrow
- - find first record (throws if not found)findMany
- - find multiple recordsfindUnique
- - find unique recordfindUniqueOrThrow
- - find unique record (throws if not found)groupBy
- - group data
Mutations (7 operations):
- create - create recordcreateMany
- - create multiple recordsdelete
- - delete recorddeleteMany
- - delete multiple recordsupdate
- - update recordupdateMany
- - update multiple recordsupsert
- - create or update record
`typescript
// Check read operations
export async function checkRead
// Check write operations
export async function checkMutate
`
The plugin automatically integrates with ZenStack RLS policies:
`zmodel
model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
// Anyone can read published posts
@@allow('read', published)
// Only authenticated users can read all posts
@@allow('read', auth() != null)
// Only author can update/delete their posts
@@allow('update,delete', auth() == author)
// Admins have full access
@@allow('all', auth().role == 'admin')
}
`
Access errors are automatically handled:
`typescript`
// Prisma error P2004 ā "Access denied"
// Prisma error P2025 ā "Not found"
| Feature | tRPC | oRPC |
|---------|------|------|
| Type Safety | ā
| ā
|
| RLS Support | Via ZenStack | Via ZenStack |
| Bundle Size | Larger | Smaller |
| Learning Curve | Steeper | Gentler |
| Middleware | Complex | Simple |
`typescript
// Generated code
export const PostRouter = {
findMany: base
.input($Schema.PostInputSchema.findMany)
.handler(async ({ context, input }) =>
checkRead(context.prisma.post.findMany(input))
),
create: base
.input($Schema.PostInputSchema.create)
.handler(async ({ context, input }) =>
checkMutate(context.prisma.post.create(input))
),
// ... +13 operations
};
`
`typescript
export const appRouter = {
user: UserRouter,
post: PostRouter,
// ... all models
};
export type AppRouter = typeof appRouter;
`
`typescript`
const posts = await orpcClient.post.findMany({
where: { published: true },
take: 10,
});
`typescript`
// RLS automatically checks permissions
const post = await orpcClient.post.create({
data: {
title: 'Hello World',
published: false,
authorId: userId,
},
});
`typescript``
try {
await orpcClient.post.delete({
where: { id: 'post-id' }
});
} catch (err) {
if (err.message === 'Access denied') {
// RLS denied the operation
}
}
Contributions are welcome! Please open an issue or PR.
MIT
- ZenStack Documentation
- oRPC Documentation
- Prisma Documentation