|
| 1 | +--- |
| 2 | +title: 'ZenStack V3: The Perfect Prisma ORM Alternative' |
| 3 | +description: Why fighting Prisma's limitations when you can have a better choice? |
| 4 | +tags: [prisma, orm] |
| 5 | +authors: yiming |
| 6 | +date: 2025-11-30 |
| 7 | +image: ./cover.png |
| 8 | +--- |
| 9 | + |
| 10 | +# ZenStack V3: The Perfect Prisma ORM Alternative |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | +[Prisma](https://www.prisma.io/) won the hearts of many developers with its excellent developer experience - the elegant schema language, the intuitive query API, and the unmatched type-safety. However, as time went by, its focus shifted, and innovation in the ORM space slowed down considerably. Many successful OSS projects indeed struggle to meet the ever-growing demands, but you'll be surprised to find that many seemingly fundamental features that have been requested for years are still missing today, such as: |
| 15 | + |
| 16 | +- [Define type of content of Json field #3219](https://github.com/prisma/prisma/issues/3219) |
| 17 | +- [Support for Polymorphic Associations #1644](https://github.com/prisma/prisma/issues/1644) |
| 18 | +- [Soft deletes (e.g. deleted_at) #3398](https://github.com/prisma/prisma/issues/3398) |
| 19 | + |
| 20 | +<!-- truncate --> |
| 21 | + |
| 22 | +As a new challenger in this space, [ZenStack](https://zenstack.dev/v3) aspires to be the spiritual successor to Prisma, but with a light-weighted architecture, a richer feature set, well-thought-out extensibility, and an easy-to-contribute codebase. Furthermore, its being essentially compatible with Prisma means you can have a smooth transition from existing projects. |
| 23 | + |
| 24 | +## A bit of history |
| 25 | + |
| 26 | +ZenStack began its journey as a power pack for Prisma ORM in late 2022. It extended Prisma's schema language with additional attributes and functions, and enhanced `PrismaClient` at runtime with extra features. The most notable enhancement is the introduction of access policies, which allow you to declaratively define fine-grained access rules in the schema had have them transparently enforced at runtime. |
| 27 | + |
| 28 | +As we went deeper along the path with v1 and v2, we felt increasingly constrained by Prisma's intrinsic limitations. We decided to make a bold change in v3: build our own ORM engine (on top of the awesome [Kysely](https://kysely.dev)) and migrate away from Prisma. To ensure an easy migration path, we've made several essential compatibility commitments: |
| 29 | + |
| 30 | +- The schema language remains compatible with (and in fact a superset of) Prisma Schema Language (PSL). |
| 31 | +- The resulting database schema remains unchanged, so no data migration is needed. |
| 32 | +- The query API stays compatible with PrismaClient. |
| 33 | +- Existing migration records continue to work without changes. |
| 34 | + |
| 35 | +## Dual API from a single schema |
| 36 | + |
| 37 | +PrismaClient's API is pleasant to use, but when you go beyond simple queries, you'll have to resort to raw SQL queries. Prisma introduced the [TypedSQL](https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql) feature to mitigate this problem. Unfortunately, it only solved half of the problem (having the query results typed), but you still have to write SQL, which is quite a drop in developer experience. |
| 38 | + |
| 39 | +ZenStack v3, thanks to the power of Kysely, offers a dual API design. You can continue to use the elegant high-level ORM queries like: |
| 40 | + |
| 41 | +```ts |
| 42 | +const users = await db.user.findMany({ |
| 43 | + where: { age: { gt: 18 } }, |
| 44 | + include: { posts: true }, |
| 45 | +}); |
| 46 | +``` |
| 47 | + |
| 48 | +Or, when your needs outgrow its power, you can seamlessly switch to Kysely's type-safe, fluent query builder API: |
| 49 | + |
| 50 | +```ts |
| 51 | +const users = await db |
| 52 | + .$qb |
| 53 | + .selectFrom('User') |
| 54 | + .where('age', '>', 18) |
| 55 | + .leftJoin('Post', 'Post.authorId', 'User.id') |
| 56 | + .select(['User.*', 'Post.title']) |
| 57 | + .execute(); |
| 58 | +``` |
| 59 | + |
| 60 | +For most applications, you'll never need to write a single line of SQL anymore. |
| 61 | + |
| 62 | +## Battery included |
| 63 | + |
| 64 | +ZenStack aims to be a battery-included ORM and solve many common data modeling/query problems in a coherent package. Here are just a few examples of how far it's come in achieving this goal. |
| 65 | + |
| 66 | +### Built-in access control |
| 67 | + |
| 68 | +Every serious application needs non-trivial authorization. Wouldn't it be great if you could consolidate all access rules in the data model and never need to worry about them at query time? Here you go: |
| 69 | + |
| 70 | +**Schema:** |
| 71 | +```zmodel |
| 72 | +model Post { |
| 73 | + id Int @id |
| 74 | + title String |
| 75 | + published Boolean |
| 76 | + author User @relation(fields: [authorId], references: [id]) |
| 77 | + authorId Int |
| 78 | +
|
| 79 | + @@deny('all', auth() == null) // deny anonymous access |
| 80 | + @@allow('all', auth() == author) // owner has full access |
| 81 | + @@allow('read', published) // published posts are publicly readable |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +**Query:** |
| 86 | +```ts |
| 87 | +async function handleRequest(req: Request) { |
| 88 | + // get the validated current user from your auth system |
| 89 | + const user = await getCurrentUser(req); |
| 90 | + |
| 91 | + // create a user-bound ORM client |
| 92 | + const userDb = db.$setAuth(user); |
| 93 | + |
| 94 | + // query with access policies automatically enforced |
| 95 | + const posts = await userDb.post.findMany(); |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +### Strongly typed JSON |
| 100 | + |
| 101 | +JSON columns are becoming increasingly popular in relational databases. Getting them typed is straightforward. |
| 102 | + |
| 103 | +**Schema**: |
| 104 | +```zmodel |
| 105 | +model User { |
| 106 | + id Int @id |
| 107 | + profile Profile @json |
| 108 | +} |
| 109 | +
|
| 110 | +type Profile { |
| 111 | + age Int |
| 112 | + bio String |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +**Query**: |
| 117 | +```ts |
| 118 | +const user = await db.user.findFirstOrThrow(); |
| 119 | +console.log(user.profile.age); // strongly typed |
| 120 | +``` |
| 121 | + |
| 122 | +### Polymorphic models |
| 123 | + |
| 124 | +Ever felt the need to model an inheritance hierarchy in the database? Polymorphic models come to the rescue. |
| 125 | + |
| 126 | +**Schema**: |
| 127 | +```zmodel |
| 128 | +model Content { |
| 129 | + id Int @id |
| 130 | + title String |
| 131 | + type String |
| 132 | +
|
| 133 | + // marks this model to be a polymorphic base with its |
| 134 | + // concrete type designated by the "type" field |
| 135 | + @@delegate(type) |
| 136 | +} |
| 137 | +
|
| 138 | +model Post extends Content { |
| 139 | + content String |
| 140 | +} |
| 141 | +
|
| 142 | +model Image extends Content { |
| 143 | + data Bytes |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +**Query**: |
| 148 | +```ts |
| 149 | +// a query with base automatically includes sub-model fields |
| 150 | +const content = await db.content.findFirstOrThrow(); |
| 151 | + |
| 152 | +// the returned type is a discriminated union |
| 153 | +if (content.type === 'Post') { |
| 154 | + // typed narrowed to `Post` |
| 155 | + console.log(content.content); |
| 156 | +} else if (content.type === 'Image') { |
| 157 | + // typed narrowed to `Image` |
| 158 | + console.log(content.data); |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +These are just some of the existing features. More cool stuff like soft deletes, audit trails, etc., will be added in the future. |
| 165 | + |
| 166 | +## Extensible from the ground up |
| 167 | + |
| 168 | +ZenStack ORM comprises three main pillars - the schema language, the CLI, and the ORM runtime. All three are designed with extensibility in mind. |
| 169 | + |
| 170 | +- The schema language allows you to add custom attributes and functions to extend its semantics freely. E.g., you can add an `@encrypted` attribute to mark fields that need encryption. |
| 171 | + |
| 172 | + ```zmodel |
| 173 | + attribute @encrypted() |
| 174 | +
|
| 175 | + model User { |
| 176 | + id Int @id |
| 177 | + email String @unique @encrypted |
| 178 | + } |
| 179 | + ``` |
| 180 | +
|
| 181 | +- The ORM runtime allows you to add plugins that can intercept queries at different levels and modify their payload or entire behavior. For the example above, you can create a plugin that recognizes the `@encrypted` attribute and transparently encrypts/decrypts field values during writes/reads. |
| 182 | +
|
| 183 | + ```ts |
| 184 | + const dbWithEncryption = db.$use( |
| 185 | + new EncryptionPlugin({ |
| 186 | + algorithm: 'AES-256-CBC', |
| 187 | + key: process.env.ENCRYPTION_KEY, |
| 188 | + }) |
| 189 | + ); |
| 190 | + ``` |
| 191 | +
|
| 192 | +- The CLI allows you to add generators that emit custom artifacts based on the schema. Think of generating an ERD diagram, or a GraphQL schema. |
| 193 | +
|
| 194 | + ```zmodel |
| 195 | + plugin erd { |
| 196 | + provider = './plugins/erd-generator' |
| 197 | + output = "./erd-diagram.md" |
| 198 | + } |
| 199 | + ``` |
| 200 | +
|
| 201 | +In fact, the access control feature mentioned earlier is entirely implemented with these extension points as a plugin package. The potential is limitless. |
| 202 | +
|
| 203 | +## Simpler, smaller footprint |
| 204 | +
|
| 205 | +ZenStack is a monorepo, 100% TypeScript project - no native binaries, no WASM modules. It keeps things lean and reduces deployment footprint. As a quick comparison, for a minimal project that uses Postgres database, the "node_modules" size difference (with `npm install --omit=dev`) is quite significant: |
| 206 | +
|
| 207 | +| ORM | "node_modules" Size | |
| 208 | +|------------|-------------------| |
| 209 | +| Prisma 7 | 224 MB | |
| 210 | +| ZenStack V3 | 33 MB | |
| 211 | +
|
| 212 | +A simpler code base also makes it easier for the community to navigate and contribute. |
| 213 | +
|
| 214 | +## Beyond ORM |
| 215 | +
|
| 216 | +A feature-rich ORM can enable some very interesting new use cases. For example, since the ORM is equipped with access control, it can be directly mapped to a service that offers a full-fledged data query API without writing any code. You effectively get a self-hosted Backend-as-a-Service, but without any vendor lock-in. Check out the [Query-as-a-Service](/docs/3.x/service) documentation if you're interested. |
| 217 | +
|
| 218 | +Furthermore, [frontend query hooks](/docs/3.x/service/client-sdk/tanstack-query/) (based on [TanStack Query](https://tanstack.com/query)) can be automatically derived, and they work seamlessly with the backend service. |
| 219 | +
|
| 220 | +All summed up, the project's goal is to be the data layer of modern full-stack applications. Kill boilerplate code, eliminate redundancy, and let your data model drive as many aspects as possible. |
| 221 | +
|
| 222 | +## Conclusion |
| 223 | +
|
| 224 | +ZenStack v3 is currently in Beta, and a production-ready version will land soon. If you're interested in trying out migrating an existing Prisma project, you can find a more thorough guide [here](/docs/3.x/migrate-prisma). Make sure to join us in [Discord](https://discord.gg/Ykhr738dUe), and we'd love to hear your feedback! |
0 commit comments