Skip to main content

Prisma Catalyst For Full-stack Development

A toolkit that supercharges Prisma ORM with a powerful access control layer, unlocking its full potential for web development.

A Power Pack for Prisma

Easy Access Control

Imperative authorization code is brittle and hard to maintain. ZenStack allows you to define access policies declaratively right inside your data model.

Auto-generated API

Data access APIs are automatically generated. Thanks to the built-in access control support, these APIs are safe to be called directly from the frontend.

E2E Type Safety

No more duplicating type definitions and syncing changes. Use one single toolkit to generate types for your entire stack, and enjoy flawless auto-completion.

From Database to UI in 4 Steps

1

Define data model

ZenStack provides a data modeling language that's a superset of Prisma schema. It adds custom attributes, functions, and standard attributes for declaring access policies and validation rules.

In this example, you can see the usage of @allow and @email attributes for attaching access policies and validation rules. These will be enforced automatically in the backend services by using an enhanced Prisma client.

Most people with Prisma experiences find it easy to pick up ZenStack. Prisma schema itself is also very intuitive, so don't worry if it's new to you.

prisma
model User {
id String @id
// @email is a field validation rule
email String @email
// @password marks field to be hashed (using bcrypt) on save
// @omit indicates the field should be filtered when the entity is returned
password String @password @omit
posts Post[]
// policy: everybody can signup
@@allow('create', true)
// policy: allow full CRUD by self
@@allow('all', auth() == this)
}
model Post {
id String @id
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
// policy: allow logged-in users to read published posts
@@allow('read', auth() != null && published)
// policy: allow full CRUD by author
// auth() is a built-in function that returns current user
@@allow('all', author == auth())
}
prisma
model User {
id String @id
// @email is a field validation rule
email String @email
// @password marks field to be hashed (using bcrypt) on save
// @omit indicates the field should be filtered when the entity is returned
password String @password @omit
posts Post[]
// policy: everybody can signup
@@allow('create', true)
// policy: allow full CRUD by self
@@allow('all', auth() == this)
}
model Post {
id String @id
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
// policy: allow logged-in users to read published posts
@@allow('read', auth() != null && published)
// policy: allow full CRUD by author
// auth() is a built-in function that returns current user
@@allow('all', author == auth())
}
2

Generate API and client code

Run the zenstack CLI to generate code from the schema, typically including Prisma schema, access policy rules, strongly typed frontend data query libraries (hooks), and tRPC routers (if you've enabled the plugin).

The toolkit is extensible, so you can write your own plugins to run custom code generation. Plugins have access to the AST parsed from the schema and can declare custom attributes and functions.

3

Mount CRUD services as an API route

To add a set of APIs that wrap around the database, use a framework-specific helper to create a request handler and install it as an API route. The example here demonstrates the integration with Next.js. Check out the documentation for guides about other frameworks.

The services are a thin wrapper around Prisma and expose all essential query and mutation operations, like findMany, create, update, aggregate, etc.

If you're a fan of tRPC, you can also use the tRPC plugin to generate routers and include them in your tRPC setup.

ts
// Next.js integration example
// pages/api/model/[...path].ts
// the standard Prisma client
const prisma = new PrismaClient();
// create a Next.js API endpoint handler
export default requestHandler({
// set up a callback to get a database instance for handling the request
getPrisma: async (req, res) => {
// get current user in the session
const user = await getSessionUser(req, res);
// return an enhanced Prisma client that enforces access policies
return withPresets(prisma, { user });
},
});
ts
// Next.js integration example
// pages/api/model/[...path].ts
// the standard Prisma client
const prisma = new PrismaClient();
// create a Next.js API endpoint handler
export default requestHandler({
// set up a callback to get a database instance for handling the request
getPrisma: async (req, res) => {
// get current user in the session
const user = await getSessionUser(req, res);
// return an enhanced Prisma client that enforces access policies
return withPresets(prisma, { user });
},
});
4

Query and mutate from the frontend

You can now use the generated frontend library (hooks) to talk to the API. The library is fully typed and offers exactly the same programming experiences as using a Prisma client on the server side. This example shows how to query in a React component.

All client requests are governed by the access policies defined in the schema, so even if you don't add any filter, the client can't accidentally read or write data that's not supposed to be allowed.

tsx
// Next.js example for a blog post list component
// components/posts.tsx
 
const Posts: FC = () => {
// "usePost" is a generated hooks method
const { findMany } = usePost();
 
// list unpublished posts together with their author's data,
// the result "posts" only include entities that are readable
// to the current user
const posts = findMany({
where: { published: false },
include: { author: true },
orderBy: { updatedAt: 'desc' },
});
 
// entities are accurately typed based on the query structure
return (
<ul>
{posts.map((post) => (
(parameter) post: Post & { author: User; }
<li key={post.id}>{post.title} by { post.author.e }</li>
                                                                 
))}
</ul>
);
};
tsx
// Next.js example for a blog post list component
// components/posts.tsx
 
const Posts: FC = () => {
// "usePost" is a generated hooks method
const { findMany } = usePost();
 
// list unpublished posts together with their author's data,
// the result "posts" only include entities that are readable
// to the current user
const posts = findMany({
where: { published: false },
include: { author: true },
orderBy: { updatedAt: 'desc' },
});
 
// entities are accurately typed based on the query structure
return (
<ul>
{posts.map((post) => (
(parameter) post: Post & { author: User; }
<li key={post.id}>{post.title} by { post.author.e }</li>
                                                                 
))}
</ul>
);
};

What's in the Whole Package

Intuitive API

Prisma provides an elegant set of APIs for talking to the database. ZenStack pushes its power further to the frontend.

E2E Type Safety

Type sharing between frontend and backend is effortless thanks to the code generation from a central schema.

Extensible Schema

Custom attributes and a plugin system give you the power to extend ZenStack's schema language to your needs.

Fullstack & Backend

Although ZenStack shines in full-stack development, it can also help you build just the backend more efficiently.

Framework Agnostic

ZenStack works with any Javascript framework. It's also easy to migrate your existing Prisma projects.

Uncompromised Performance

Access policies and validation rules are precompiled and pushed down to the database engine whenever possible.