From 34cb96943928f6f20b2d6ad85e83ed228ec28c52 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 13 Jun 2026 03:59:08 +0000 Subject: [PATCH 01/16] docs: fix missing search on mobile, cleanup navbar --- docusaurus.config.js | 45 +++++----------------------------------- src/css/custom.css | 49 -------------------------------------------- 2 files changed, 5 insertions(+), 89 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 71684e1d..5105d22b 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -107,6 +107,11 @@ const config = { label: 'Reference', }, { to: '/blog', label: 'Blog', position: 'left' }, + { + href: 'https://github.com/zenstackhq/zenstack', + label: 'GitHub', + position: 'right', + }, { href: 'https://discord.gg/Ykhr738dUe', label: 'Discord', @@ -116,46 +121,6 @@ const config = { type: 'docsVersionDropdown', position: 'right', }, - { - type: 'html', - position: 'left', - value: `
- - - - - - Star on Github - - - - - - -
`, - }, ], }, footer: { diff --git a/src/css/custom.css b/src/css/custom.css index 3559aeea..c6dfd23d 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -36,52 +36,3 @@ .footer { @apply px-8 lg:px-16; } - -.navbar__item:has(> #github-button) { - display: inline-block; -} - -@media screen and (max-width: 445px) { - #github-text { - display: none; - } -} - -#github-button::before { - content: ''; - position: absolute; - top: 0; - right: -100%; - bottom: 0; - width: 100%; - opacity: 0.1; - background: var(--ifm-navbar-background-color); - transition: right 0.5s; - pointer-events: none; -} - -[data-theme='dark'] #github-button::before { - opacity: 0.3; -} - -#github-button:hover::before { - right: 0; - pointer-events: mouse; -} - -@media (max-width: 1200px) { - .navbar__items--right, - .navbar__items--left { - display: none; - } - - .navbar__toggle { - display: inherit; - } -} - -@media (min-width: 1201px) { - .navbar__toggle { - display: none; - } -} From fc86f3e7004a862a5229a7934abc831c891036a8 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 13 Jun 2026 04:22:51 +0000 Subject: [PATCH 02/16] update styling --- docusaurus.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 5105d22b..ed2188a9 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -109,8 +109,9 @@ const config = { { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/zenstackhq/zenstack', - label: 'GitHub', + label: '⭐ GitHub', position: 'right', + className: 'bg-gray-900 rounded-sm', }, { href: 'https://discord.gg/Ykhr738dUe', From 83e11082acda3e6fa01184eb95df8fdf7543efe1 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:00:33 -0700 Subject: [PATCH 03/16] docs: add PGlite section (#617) * docs: add PGlite section * add newline --- docs/recipe/databases/postgres.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/recipe/databases/postgres.md b/docs/recipe/databases/postgres.md index 15c0de24..fbad0498 100644 --- a/docs/recipe/databases/postgres.md +++ b/docs/recipe/databases/postgres.md @@ -26,3 +26,24 @@ const db = new ZenStackClient(schema, { }), }); ``` + +## Using PGlite + +:::danger No Official Support +The PGlite dialect is not officially supported or tested by ZenStack, but you may evaluate it and [report your findings and interest on the GitHub issue](https://github.com/zenstackhq/zenstack/issues/2710). +::: + + + +```ts +import { schema } from './zenstack/schema'; +import { PGlite } from '@electric-sql/pglite'; +import { ZenStackClient } from '@zenstackhq/orm'; +import { PGliteDialect } from 'kysely'; + +const db = new ZenStackClient(schema, { + dialect: new PGliteDialect({ + pglite: new PGlite(), + }), +}); +``` From 0c9db881a561f3c527123116eb0a041d98bc4b88 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 17 Jun 2026 23:39:02 -0700 Subject: [PATCH 04/16] doc: release 3.8.0 (#619) * doc: release 3.8.0 * update --- docs/reference/plugins/soft-delete.md | 149 ++++++++++++++++++++++ docs/reference/zmodel/input-validation.md | 40 ++++++ 2 files changed, 189 insertions(+) create mode 100644 docs/reference/plugins/soft-delete.md diff --git a/docs/reference/plugins/soft-delete.md b/docs/reference/plugins/soft-delete.md new file mode 100644 index 00000000..cad6bb54 --- /dev/null +++ b/docs/reference/plugins/soft-delete.md @@ -0,0 +1,149 @@ +--- +sidebar_position: 4 +--- + +import AvailableSince from '../../_components/AvailableSince'; + +# @zenstackhq/plugin-soft-delete + + + +The `@zenstackhq/plugin-soft-delete` plugin implements **soft delete** by intercepting Kysely queries at runtime. Instead of physically removing rows, delete operations mark them with a timestamp, and reads automatically exclude the marked rows. + +## How It Works + +The plugin works off a single `@deletedAt` marker field on each model that should support soft deletion: + +- **Deletes become updates** — a `delete`/`deleteMany` against a soft-delete model is rewritten to set the `@deletedAt` field to the current timestamp instead of issuing a `DELETE`. +- **Reads are filtered** — `find*` queries (and joined relations) automatically add a ` IS NULL` condition, so soft-deleted rows are invisible. +- **Updates skip tombstones** — `update`/`updateMany` won't touch rows that are already soft-deleted. + +Models without a `@deletedAt` field are left completely untouched. + +## Installation + +```bash +npm install @zenstackhq/plugin-soft-delete +``` + +## Usage + +### 1. Declare the plugin in your ZModel schema + +Declaring the plugin makes the `@deletedAt` attribute available in your schema. + +```zmodel +plugin softDelete { + provider = '@zenstackhq/plugin-soft-delete' +} +``` + +### 2. Mark a nullable `DateTime` field with `@deletedAt` + +A model can have at most one `@deletedAt` field, and it must be optional (so that "not deleted" is represented by `null`). + +```zmodel +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] + deletedAt DateTime? @deletedAt +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + deletedAt DateTime? @deletedAt +} +``` + +### 3. Install the plugin on your client at runtime + +```ts +import { ZenStackClient } from '@zenstackhq/orm'; +import { SoftDeletePlugin } from '@zenstackhq/plugin-soft-delete'; +import { schema } from './schema'; + +const db = new ZenStackClient(schema, { ... }).$use(new SoftDeletePlugin()); + +const user = await db.user.create({ data: { email: 'a@example.com' } }); + +// rewritten to set `deletedAt` — the row is kept in the database +await db.user.delete({ where: { id: user.id } }); + +// returns `null` — soft-deleted rows are hidden from reads +await db.user.findUnique({ where: { id: user.id } }); +``` + +### Works with the query builder APIs + +Because the plugin intercepts queries at the Kysely level, soft-delete behavior also applies to the low-level [query builder](../../orm/query-builder.md) escape hatch (`$qb`), not just the ORM API. Deletes are rewritten to `@deletedAt` updates and reads are filtered there too. + +```ts +// rewritten to set `deletedAt` instead of issuing a DELETE +await db.$qb.deleteFrom('User').where('id', '=', user.id).execute(); + +// only returns rows where `deletedAt IS NULL` +await db.$qb.selectFrom('User').selectAll().execute(); +``` + +## ZModel Declarations + +### Attributes + +#### `@deletedAt` + +```zmodel +attribute @deletedAt() +``` + +Marks the field used as the soft-delete tombstone marker. The field must be an optional `DateTime?`. A model may declare at most one `@deletedAt` field. + +## Caveats + +- **Soft deletes do not cascade.** Children of a soft-deleted parent are left untouched — managing them is up to you. (Note that a *hard* delete on a model without `@deletedAt` still triggers database-level `onDelete: Cascade` as usual.) +- **Multi-table / joined deletes can't be rewritten.** A joined or multi-table `DELETE` that targets a soft-delete model is rejected rather than silently hard-deleting rows. Use a single-table delete instead. +- **Unique constraints and tombstones.** Because soft-deleted rows physically remain, a plain `@unique` field will reject reusing a value held by a tombstone. See [Reusing unique values](#reusing-unique-values) below for the mitigation. + +## Reusing unique values + +A `@unique` field in ZModel compiles to a regular database unique constraint that also covers soft-deleted rows. So once a user with `email = "a@example.com"` is soft-deleted, you can't create another user with the same email — the tombstone still occupies that value. + +The fix is a **partial (filtered) unique index** scoped to live rows (`deletedAt IS NULL`). ZModel can't express this, so you add it through a **manually created migration**. Generate an empty migration with `--create-only`, then edit its SQL: + +```bash +npx zenstack migrate dev --create-only --name soft_delete_unique_email +``` + +In the generated migration, drop the plain unique constraint and replace it with a partial one. The exact SQL depends on your database: + +**PostgreSQL** — supports partial indexes directly: + +```sql +CREATE UNIQUE INDEX "User_email_active_key" + ON "User" ("email") + WHERE "deletedAt" IS NULL; +``` + +**SQLite** — also supports partial indexes: + +```sql +CREATE UNIQUE INDEX "User_email_active_key" + ON "User" ("email") + WHERE "deletedAt" IS NULL; +``` + +**MySQL** — has no partial indexes, but a unique index allows multiple `NULL`s, so use it over an expression that is the value only for live rows (and `NULL` for tombstones): + +```sql +ALTER TABLE `User` + ADD UNIQUE INDEX `User_email_active_key` ( + (CASE WHEN `deletedAt` IS NULL THEN `email` END) + ); +``` + +:::caution +Migrations are diff-based, so if you leave `@unique` on the field the next `migrate dev` will detect the plain index as "missing" and try to recreate it. To keep the schema and database in sync, drop `@unique` from the field in ZModel and let the manual partial index enforce uniqueness instead. +::: diff --git a/docs/reference/zmodel/input-validation.md b/docs/reference/zmodel/input-validation.md index 71f8ca10..535fde14 100644 --- a/docs/reference/zmodel/input-validation.md +++ b/docs/reference/zmodel/input-validation.md @@ -91,6 +91,26 @@ All field-level attributes have a `message` parameter that allows you to provide Requires a string field to be a valid ISO 8601 datetime. + - `@date` + + + + ```zmodel + @date(_ message: String?) + ``` + + Requires a string field to be a valid ISO 8601 date (e.g. `2024-01-31`). + + - `@time` + + + + ```zmodel + @time(_ precision: Int?, _ message: String?) + ``` + + Requires a string field to be a valid ISO 8601 time (e.g. `14:30:00`). The optional `precision` argument constrains the number of fractional-second digits allowed. + - `@regex` ```zmodel @@ -233,6 +253,26 @@ All field-level attributes have a `message` parameter that allows you to provide Checks if a string field is a valid ISO 8601 datetime. +- `isDate()` + + + + ```zmodel + function isDate(field: String): Boolean {} + ``` + + Checks if a string field is a valid ISO 8601 date. + +- `isTime()` + + + + ```zmodel + function isTime(field: String, precision: Int?): Boolean {} + ``` + + Checks if a string field is a valid ISO 8601 time. The optional `precision` argument constrains the number of fractional-second digits allowed. + - `regex()` ```zmodel From b419ca1e94160628ce1783b3a03a5f49061d3fca Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 18 Jun 2026 09:36:32 -0700 Subject: [PATCH 05/16] docs: mark soft-delete plugin as preview (#620) Co-authored-by: Claude Opus 4.8 --- docs/reference/plugins/soft-delete.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/plugins/soft-delete.md b/docs/reference/plugins/soft-delete.md index cad6bb54..bf69c922 100644 --- a/docs/reference/plugins/soft-delete.md +++ b/docs/reference/plugins/soft-delete.md @@ -3,11 +3,14 @@ sidebar_position: 4 --- import AvailableSince from '../../_components/AvailableSince'; +import PreviewFeature from '../../_components/PreviewFeature'; # @zenstackhq/plugin-soft-delete + + The `@zenstackhq/plugin-soft-delete` plugin implements **soft delete** by intercepting Kysely queries at runtime. Instead of physically removing rows, delete operations mark them with a timestamp, and reads automatically exclude the marked rows. ## How It Works From 40b50edeec917e0b4cc8b23bc06d4d000c9a1c66 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 18 Jun 2026 10:01:19 -0700 Subject: [PATCH 06/16] docs: add zenstack-mcp to community packages (#621) Co-authored-by: Claude Opus 4.8 --- docs/community-packages.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/community-packages.md b/docs/community-packages.md index fa2cfc89..211dac6b 100644 --- a/docs/community-packages.md +++ b/docs/community-packages.md @@ -7,6 +7,10 @@ sidebar_label: Community Packages ZenStack plugins built by our awesome community members :heart:. +- [zenstack-mcp](https://github.com/azzerty23/zenstack-mcp) + + Expose your ZenStack V3 database to LLMs through a [Model Context Protocol](https://modelcontextprotocol.io) interface, with built-in access-control policies and OAuth 2.0 authentication. Made by [Azzerty23](https://github.com/azzerty23). + - [zenstack-replicas](https://github.com/visualbravo/zenstack-replicas) Distribute read operations across database replicas with round-robin load balancing for the ZenStack ORM. Made by [@sanny-io](https://github.com/sanny-io). From bfc98b9f08b9f2195314aca8583ce7ceb436f99c Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 3 Jul 2026 06:06:52 +0800 Subject: [PATCH 07/16] docs: clarify omit is not a security feature (#622) Co-authored-by: Claude Opus 4.8 (1M context) --- docs/orm/api/omit.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/orm/api/omit.md b/docs/orm/api/omit.md index 5baaf769..04e3b817 100644 --- a/docs/orm/api/omit.md +++ b/docs/orm/api/omit.md @@ -13,6 +13,10 @@ The previous sections have shown how you can use `select` and `omit` clauses to Please note that the omit settings only affect the ORM query APIs, but not the [Query Builder APIs](../query-builder). With query builder you'll need to explicitly specify the fields to select. ::: +:::warning +Omit (at all three layers) is not a security feature. It's merely a convenient way to control the default set of fields returned during a query. Don't rely on it to protect sensitive data — use [access policies](../access-control) for that purpose. +::: + ## Schema-Level Omit You can use the `@omit` attribute in ZModel to mark fields to be omitted. Such fields will be omitted by default for all `ZenStackClient` instances and all queries. @@ -71,7 +75,7 @@ const users = await db.user.findMany({ }); ``` -There might be scenarios where you don't want the query-level override feature. For example, when using `ZenStackClient` with the [Query as a Service](../../service), you may want to have certain fields always omitted for security reasons. In such cases, use the `allowQueryTimeOmitOverride` option to disable query-time overrides: +There might be scenarios where you don't want the query-level override feature. For example, when using `ZenStackClient` with the [Query as a Service](../../service), you may want to have certain fields always omitted. In such cases, use the `allowQueryTimeOmitOverride` option to disable query-time overrides: ```ts const db = new ZenStackClient({ From 039a12f54e5aaf2a716cc285f814c55db5d1c1e2 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 5 Jul 2026 11:06:02 +0800 Subject: [PATCH 08/16] chore: new logo (#623) --- static/img/zenstack-new-logo.png | Bin 0 -> 489 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/img/zenstack-new-logo.png diff --git a/static/img/zenstack-new-logo.png b/static/img/zenstack-new-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2139ee7060f1163453595027d36dbbab88cff4b8 GIT binary patch literal 489 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$a>caTa()7BevDc!MzGQrl@Ofr5<1 zLGDfr>(0r%1aer?9eo`c7&i8E|4C#8^7%?!BT9nv(@M${i&7cfGShPt=WU#p4Ahn4 z>Eakt!T9#ZM&3gXA}kl{*Y#YAo!QT}fy0d>vP%DM$)dT8Km! Date: Sun, 5 Jul 2026 11:14:22 +0800 Subject: [PATCH 09/16] Chore/new logo (#624) * chore: new logo * update logo --- static/img/new-logo.png | Bin 0 -> 684 bytes static/img/zenstack-new-logo.png | Bin 489 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/img/new-logo.png delete mode 100644 static/img/zenstack-new-logo.png diff --git a/static/img/new-logo.png b/static/img/new-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..53f3bf122edc8c78c9e7b10dbcb6c743eb469811 GIT binary patch literal 684 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz@Xy|!i-C8r>z7j zDGqXXVpw-h<|UBBlJ4m1$iT3%pZiZDE0E7u;u=vBoS#-wo>-L1;Fg)5n>cUdv}6Vb zCUs92$B+ufx3@Nm9&+Geb)2hR{BqqHcUF}T93ReIQONz04%AK&c)*>#=v(ERhigmk z-n^}0$#z1BfkSx6Ac)s1=9vCDV%Kl`r?>R($@mS-4F-%u2vHP$Ak7bq^<#`V-Z%d} QvJb@dboFyt=akR{0D-V`7XSbN literal 0 HcmV?d00001 diff --git a/static/img/zenstack-new-logo.png b/static/img/zenstack-new-logo.png deleted file mode 100644 index 2139ee7060f1163453595027d36dbbab88cff4b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 489 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$a>caTa()7BevDc!MzGQrl@Ofr5<1 zLGDfr>(0r%1aer?9eo`c7&i8E|4C#8^7%?!BT9nv(@M${i&7cfGShPt=WU#p4Ahn4 z>Eakt!T9#ZM&3gXA}kl{*Y#YAo!QT}fy0d>vP%DM$)dT8Km! Date: Sun, 5 Jul 2026 11:22:17 +0800 Subject: [PATCH 10/16] chore: add full logo (#625) --- static/img/new-logo-full.png | Bin 0 -> 5324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/img/new-logo-full.png diff --git a/static/img/new-logo-full.png b/static/img/new-logo-full.png new file mode 100644 index 0000000000000000000000000000000000000000..c47e5b6bc747694c37fcc98d44cd4fcb29279529 GIT binary patch literal 5324 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 Date: Sun, 12 Jul 2026 21:22:59 +0800 Subject: [PATCH 11/16] chore: new branding and Framer-hosted landing page (#627) * chore: update theme to match new branding - Change primary color to #ff3f02 (matching zenstack.dev/framer) - Use IBM Plex Sans as the base font - Style navbar title in orange, all caps, tighter logo gap - Replace logo.png/logo-dark.png with new-logo.png Co-Authored-By: Claude Fable 5 * chore: rewrite root to framer landing page Co-Authored-By: Claude Fable 5 * chore: move custom home page to /home so root rewrite takes effect Vercel rewrites only apply when no static file matches the path, so the Docusaurus-generated root index.html was shadowing the / -> Framer rewrite. Co-Authored-By: Claude Fable 5 * chore: remove old home page and /framer rewrites The custom home page (moved to /home earlier) is superseded by the Framer-hosted landing page served at the root. Sponsorship/UserLogos components were only used by it. Co-Authored-By: Claude Fable 5 * update framer custom domain --------- Co-authored-by: Claude Fable 5 --- blog/react-table/index.md | 2 +- docusaurus.config.js | 13 ++- src/components/Sponsorship.tsx | 30 ------- src/components/UserLogos.tsx | 79 ----------------- src/css/custom.css | 39 ++++++--- src/pages/_components/AICoding.tsx | 69 --------------- src/pages/_components/Notes.tsx | 22 ----- src/pages/_components/ORM.tsx | 62 -------------- src/pages/_components/Schema.tsx | 55 ------------ src/pages/_components/Service.tsx | 112 ------------------------ src/pages/_components/ValueProps.tsx | 56 ------------ src/pages/index.module.css | 24 ------ src/pages/index.tsx | 123 --------------------------- vercel.json | 8 +- 14 files changed, 38 insertions(+), 656 deletions(-) delete mode 100644 src/components/Sponsorship.tsx delete mode 100644 src/components/UserLogos.tsx delete mode 100644 src/pages/_components/AICoding.tsx delete mode 100644 src/pages/_components/Notes.tsx delete mode 100644 src/pages/_components/ORM.tsx delete mode 100644 src/pages/_components/Schema.tsx delete mode 100644 src/pages/_components/Service.tsx delete mode 100644 src/pages/_components/ValueProps.tsx delete mode 100644 src/pages/index.module.css delete mode 100644 src/pages/index.tsx diff --git a/blog/react-table/index.md b/blog/react-table/index.md index 2ea5b81d..b4416720 100644 --- a/blog/react-table/index.md +++ b/blog/react-table/index.md @@ -19,7 +19,7 @@ For two reasons, tables are one of the nastiest things to build in web UI. First > Building UI is a very branded and custom experience, even if that means choosing a design system or adhering to a design spec. - tanstack.com -Tables are most commonly used to render database query results — in modern times, the output of an ORM. In this post, I'll introduce a way of connecting [Prisma](https://prisma.io) - the most popular TypeScript ORM, to React Table, with the help of [React Query](https://tanstack.com/query) and [ZenStack](/). You'll be amazed by how little code you need to write to render a full-fledged table UI. +Tables are most commonly used to render database query results — in modern times, the output of an ORM. In this post, I'll introduce a way of connecting [Prisma](https://prisma.io) - the most popular TypeScript ORM, to React Table, with the help of [React Query](https://tanstack.com/query) and [ZenStack](https://zenstack.dev). You'll be amazed by how little code you need to write to render a full-fledged table UI. ## A full-stack setup diff --git a/docusaurus.config.js b/docusaurus.config.js index ed2188a9..ee812af1 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -10,7 +10,7 @@ const config = { baseUrl: '/', onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', - favicon: 'img/logo.png', + favicon: 'img/new-logo.png', // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. @@ -25,6 +25,13 @@ const config = { locales: ['en'], }, + stylesheets: [ + { + href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap', + type: 'text/css', + }, + ], + presets: [ [ 'classic', @@ -91,8 +98,8 @@ const config = { title: 'ZenStack', logo: { alt: 'ZenStack Logo', - src: 'img/logo.png', - srcDark: 'img/logo-dark.png', + src: 'img/new-logo.png', + href: 'pathname:///', }, items: [ { diff --git a/src/components/Sponsorship.tsx b/src/components/Sponsorship.tsx deleted file mode 100644 index 71dc61f9..00000000 --- a/src/components/Sponsorship.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -export default function Sponsorship(): JSX.Element { - return ( -
-
-

Our Generous Sponsors

-
- - - - -
-
-
- ); -} - -function Sponsor({ src, name, website }: { src: string; name: string; website: string }): JSX.Element { - const alt = src.split('/').pop()?.split('.')[0] ?? 'logo'; - return ( - - {alt} -

{name}

-
- ); -} diff --git a/src/components/UserLogos.tsx b/src/components/UserLogos.tsx deleted file mode 100644 index d9068967..00000000 --- a/src/components/UserLogos.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; - -interface UserLogoProps { - src: string; - name: string; - website: string; - className?: string; - style?: React.CSSProperties; - imageStyle?: React.CSSProperties; - darkSrc?: string; -} - -function UserLogo({ src, name, website, className, style, imageStyle, darkSrc }: UserLogoProps): JSX.Element { - return ( -
- {name} - {name} - - {name} - -
- ); -} - -export default function UserLogs(): JSX.Element { - return ( -
-
-

Used and Loved by

-
- - - - - - - -
-
-
- ); -} diff --git a/src/css/custom.css b/src/css/custom.css index c6dfd23d..96fdf67d 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -10,29 +10,40 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #ff7100; - --ifm-color-primary-dark: #e66600; - --ifm-color-primary-darker: #cc5a00; - --ifm-color-primary-darkest: #b34f00; - --ifm-color-primary-light: #ff7f1a; - --ifm-color-primary-lighter: #ff8d33; - --ifm-color-primary-lightest: #ff9c4d; + --ifm-color-primary: #ff3f02; + --ifm-color-primary-dark: #e63902; + --ifm-color-primary-darker: #cc3202; + --ifm-color-primary-darkest: #b32c01; + --ifm-color-primary-light: #ff521b; + --ifm-color-primary-lighter: #ff6535; + --ifm-color-primary-lightest: #ff794e; + --ifm-font-family-base: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 148, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { - --ifm-color-primary: #ff7f1a; - --ifm-color-primary-dark: #ff7100; - --ifm-color-primary-darker: #e66600; - --ifm-color-primary-darkest: #cc5a00; - --ifm-color-primary-light: #ff8d33; - --ifm-color-primary-lighter: #ff9c4d; - --ifm-color-primary-lightest: #ffaa66; + --ifm-color-primary: #ff521b; + --ifm-color-primary-dark: #ff3f02; + --ifm-color-primary-darker: #e63902; + --ifm-color-primary-darkest: #cc3202; + --ifm-color-primary-light: #ff6535; + --ifm-color-primary-lighter: #ff794e; + --ifm-color-primary-lightest: #ff8c67; --docusaurus-highlighted-code-line-bg: rgba(0, 148, 0, 0.3); } +.navbar__logo { + margin-right: 0.125rem; +} + +.navbar__title { + color: var(--ifm-color-primary); + text-transform: uppercase; +} + .footer { @apply px-8 lg:px-16; } diff --git a/src/pages/_components/AICoding.tsx b/src/pages/_components/AICoding.tsx deleted file mode 100644 index aad73f88..00000000 --- a/src/pages/_components/AICoding.tsx +++ /dev/null @@ -1,69 +0,0 @@ -type FeatureItem = { - title: string; - img: string; - description: JSX.Element; -}; - -const FeatureList: FeatureItem[] = [ - { - title: 'Single Source of Truth', - img: '/img/access-control.png', - description: ( - <> - When LLMs see a self-contained, non-ambiguous, and well-defined application model, their inference works - more efficiently and effectively. - - ), - }, - { - title: 'Concise Query API', - img: '/img/auto-api.png', - description: ( - <> - Concise and expressive, while leveraging existing knowledge of Prisma and Kysely, the query API makes it - easy for LLMs to generate high-quality query code. - - ), - }, - { - title: 'Slim Code Base', - img: '/img/ai-friendly.png', - description: ( - <> - By deriving artifacts from the schema instead of implementing them, ZenStack helps you maintain a slim - code base that is easier for AI to digest. - - ), - }, -]; - -function Proposition({ title, img, description }: FeatureItem) { - return ( -
-
- {title} -
-
-

{title}

-

{description}

-
-
- ); -} - -export default function AICoding(): JSX.Element { - return ( -
-
-

- Perfect Match for AI-Assisted Programming -

-
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
- ); -} diff --git a/src/pages/_components/Notes.tsx b/src/pages/_components/Notes.tsx deleted file mode 100644 index 6bcfbb83..00000000 --- a/src/pages/_components/Notes.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Link from '@docusaurus/Link'; - -export default function Notes(): JSX.Element { - return ( -
-
-

- Notes to V2 Users -

-
-
-
- ZenStack V3 has made the bold decision to remove Prisma as a runtime dependency and implement its - own ORM infrastructure on top of Kysely. Albeit the cost of such a - big refactor, we believe this is the right move to gain the flexibility needed to achieve the - project's vision. Please read this blog post for more - thoughts behind the changes. -
-
-
- ); -} diff --git a/src/pages/_components/ORM.tsx b/src/pages/_components/ORM.tsx deleted file mode 100644 index 2d9bcfdb..00000000 --- a/src/pages/_components/ORM.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import CodeBlock from '@theme/CodeBlock'; - -export default function ORM(): JSX.Element { - return ( -
-
-

-
Flexible and Awesomely Typed ORM
-

-
-
- - {`import { schema } from './zenstack'; -import { ZenStackClient } from '@zenstackhq/orm'; -import { PolicyPlugin } from '@zenstackhq/plugin-policy'; - -const db = new ZenStackClient(schema, { ... }) - // install access control plugin to enforce policies - .$use(new PolicyPlugin()) - // set current user context - .$setAuth(...); - -// high-level query API -const userWithPosts = await db.user.findUnique({ - where: { id: userId }, - include: { posts: true } -}); - -// low-level SQL query builder API -const userPostJoin = await db - .$qb - .selectFrom('User') - .innerJoin('Post', 'Post.authorId', 'User.id') - .select(['User.id', 'User.email', 'Post.title']) - .where('User.id', '=', userId) - .execute(); -`} - -
-

- An ORM is derived from the schema that gives you -

-
    -
  • 🔋 High-level ORM query API
  • -
  • 🔋 Low-level SQL query builder API
  • -
  • 🔋 Access control enforcement
  • -
  • 🔋 Runtime data validation
  • -
  • 🔋 Computed fields and custom procedures
  • -
  • 🔋 Plugin system for tapping into various lifecycle events
  • -
- - ZenStack's ORM is built on top of the awesome Kysely SQL query - builder. Its query API is compatible with that of{' '} - Prisma Client, so migrating an - existing Prisma project will require minimal code changes.{' '} - Read more about migrating from Prisma. - -
-
-
- ); -} diff --git a/src/pages/_components/Schema.tsx b/src/pages/_components/Schema.tsx deleted file mode 100644 index 04fc4a59..00000000 --- a/src/pages/_components/Schema.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import CodeBlock from '@theme/CodeBlock'; - -export default function SchemaLanguage(): JSX.Element { - return ( -
-
-

-
Intuitive and Expressive Data Modeling
-

-
-
-
-

The modeling language allows you to

-
    -
  • ✅ Define data models and relations
  • -
  • ✅ Define access control policies
  • -
  • ✅ Express data validation rules
  • -
  • ✅ Model polymorphic inheritance
  • -
  • ✅ Add custom attributes and functions to introduce custom semantics
  • -
  • ✅ Implement custom code generators
  • -
- - The schema language is a superset of{' '} - Prisma Schema Language. - Migrating a Prisma schema is as simple as file renaming. - -
- - {`model User { - id Int @id - email String @unique @email // constraint and validation - role String - posts Post[] // relation to another model - postCount Int @computed // computed field - - // access control rules colocated with data - @@allow('all', auth().id == id) - @@allow('create, read', true) -} - -model Post { - id Int @id - title String @length(1, 255) - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int // relation foreign key - - @@allow('read', published) - @@allow('all', auth().id == authorId || auth().role == 'ADMIN') -}`} - -
-
- ); -} diff --git a/src/pages/_components/Service.tsx b/src/pages/_components/Service.tsx deleted file mode 100644 index 73b1975c..00000000 --- a/src/pages/_components/Service.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import CodeBlock from '@theme/CodeBlock'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -export default function Service(): JSX.Element { - return ( -
-
-

-
Automatic HTTP Query Service
{' '} -

-
-
-
-

- Thanks to the ORM's built-in access control, you get an HTTP query service for free -

-
    -
  • 🚀 Fully mirrors the ORM API
  • -
  • 🚀 Seamlessly integrates with popular frameworks
  • -
  • 🚀 Works with any authentication solution
  • -
  • - 🚀 Type-safe client SDK powered by{' '} - - TanStack Query - -
  • -
  • 🚀 Highly customizable
  • -
-
-

- Since the ORM is protected with access control, ZenStack can directly map it to an HTTP - service. ZenStack provides out-of-the-box integrations with popular frameworks including - Next.js, Nuxt, Express, etc. -

-

- Client hooks based on{' '} - - TanStack Query - {' '} - can also be derived from the schema, allowing you to make type-safe queries to the service - without writing a single line of code. -

-
-
-
- - - - {`import { NextRequestHandler } from '@zenstackhq/server/next'; -import { db } from './db'; // ZenStackClient instance -import { getSessionUser } from './auth'; - -// callback to provide a per-request ORM client -async function getClient() { - // call a framework-specific helper to get session user - const authUser = await getSessionUser(); - - // return a new ORM client configured with the user, - // the user info will be used to enforce access control - return db.$setAuth(authUser); -} - -// Create a request handler for all requests to this route -// All CRUD requests are forwarded to the underlying ORM -const handler = NextRequestHandler({ getClient }); - -export { - handler as GET, - handler as PUT, - handler as POST, - handler as PATCH, - handler as DELETE, -}; - `} - - - - - {`import { schema } from './zenstack'; -import { useClientQueries } from '@zenstackhq/tanstack-query/react'; - -export function UserPosts({ userId }: { userId: number }) { - // use auto-generated hook to query user with posts - const client = useClientQueries(schema); - const { data, isLoading } = client.user.useFindUnique({ - where: { id: userId }, - include: { posts: true } - }); - - if (isLoading) return
Loading...
; - - return ( -
-

{data?.email}'s Posts

-
    - {data?.posts.map((post) => ( -
  • {post.title}
  • - ))} -
-
- ); -} - `} -
-
-
-
-
-
- ); -} diff --git a/src/pages/_components/ValueProps.tsx b/src/pages/_components/ValueProps.tsx deleted file mode 100644 index 555ecdad..00000000 --- a/src/pages/_components/ValueProps.tsx +++ /dev/null @@ -1,56 +0,0 @@ -type FeatureItem = { - title: string; - img: string; - description: JSX.Element; -}; - -const FeatureList: FeatureItem[] = [ - { - title: 'Coherent Schema', - img: '/img/diagram.png', - description: ( - <> - Simple schema language to capture the most important aspects of your application in one place: data and - security. - - ), - }, - { - title: 'Powerful ORM', - img: '/img/search.png', - description: ( - <>One-of-a-kind ORM that combines type-safety, query flexibility, and access control in one package. - ), - }, - { - title: 'Limitless Utility', - img: '/img/versatility.png', - description: ( - <>Deriving crucial artifacts that streamline development from backend APIs to frontend components. - ), - }, -]; - -function Proposition({ title, img, description }: FeatureItem) { - return ( -
-
- {title} -
-
-

{title}

-

{description}

-
-
- ); -} - -export default function ValueProps(): JSX.Element { - return ( -
- {FeatureList.map((props, idx) => ( - - ))} -
- ); -} diff --git a/src/pages/index.module.css b/src/pages/index.module.css deleted file mode 100644 index b3332046..00000000 --- a/src/pages/index.module.css +++ /dev/null @@ -1,24 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 8rem 2rem !important; - text-align: center !important; - position: relative !important; - overflow: hidden !important; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding-top: 4rem !important; - padding-bottom: 4rem !important; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/pages/index.tsx b/src/pages/index.tsx deleted file mode 100644 index 36f6fc32..00000000 --- a/src/pages/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import Layout from '@theme/Layout'; -import clsx from 'clsx'; -import React from 'react'; -import Sponsorship from '../components/Sponsorship'; -import UserLogs from '../components/UserLogos'; -import AICoding from './_components/AICoding'; -import ORM from './_components/ORM'; -import SchemaLanguage from './_components/Schema'; -import Service from './_components/Service'; -import ValueProps from './_components/ValueProps'; -import styles from './index.module.css'; - -const description = `ZenStack is a powerful data layer for modern TypeScript applications. It provides an intuitive data modeling language, a fully type-safe ORM, built-in access control and data validation, and automatic data query service that seamlessly integrates with popular frameworks like Next.js and Nuxt.`; - -function Header() { - return ( -
-
-
-
-

- - Modern Data Layer for TypeScript Applications - -

-

- Intuitive data modeling, type-safe ORM, built-in access control, automatic query services, - and more. -

-
- - Get Started → - - - Open Playground - -
-
-
-
-
- ); -} - -function Section({ children, className }: { children: React.ReactNode; className?: string }) { - return ( -
-
{children}
-
- ); -} - -export default function Home(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - return ( - -
-
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - Start Building Now → - -
-
- -
- -
- - Support this project → - -
-
- - {/*
- -
*/} -
- - ); -} diff --git a/vercel.json b/vercel.json index 0bec30f8..7b1be7fe 100644 --- a/vercel.json +++ b/vercel.json @@ -2,12 +2,8 @@ "trailingSlash": false, "rewrites": [ { - "source": "/framer", - "destination": "https://detailed-costs-055147.framer.app/" - }, - { - "source": "/framer/:path*", - "destination": "https://detailed-costs-055147.framer.app/:path*" + "source": "/", + "destination": "https://zenstack.framer.website/" } ], "redirects": [ From 02c67bb1a261c39f42a98c2d9bc6fc353cc4fa6a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 12 Jul 2026 21:43:54 +0800 Subject: [PATCH 12/16] fix: open navbar logo home link in the same tab (#628) Co-authored-by: Claude Fable 5 --- docusaurus.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docusaurus.config.js b/docusaurus.config.js index ee812af1..11d88a34 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -100,6 +100,7 @@ const config = { alt: 'ZenStack Logo', src: 'img/new-logo.png', href: 'pathname:///', + target: '_self', }, items: [ { From 3592c2f5b6fcbcb1500b3a24eb31777bb9a86203 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 15 Jul 2026 10:18:51 +0800 Subject: [PATCH 13/16] fix: rewrite canonical URL of Framer-hosted homepage to zenstack.dev (#629) * fix: rewrite canonical URL of Framer-hosted homepage to zenstack.dev The / route is rewritten to zenstack.framer.website (vercel.json), so the page served at zenstack.dev declared the Framer domain as its canonical and og:url. Add Vercel Edge Middleware that proxies the Framer page and rewrites those URLs to https://zenstack.dev. On any failure it falls through to the existing plain rewrite. Co-Authored-By: Claude Fable 5 * fix: remove log * fix: forward query string and safe request headers to Framer upstream Co-Authored-By: Claude Fable 5 * fix: forward referer and x-forwarded-for headers to Framer upstream Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- middleware.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 middleware.ts diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 00000000..43b89b42 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,50 @@ +/** + * Vercel Edge Middleware. + * + * The `/` route is rewritten to the Framer-hosted landing page (see + * `vercel.json`), whose HTML declares `zenstack.framer.website` as the + * canonical/og:url. This middleware proxies the page and rewrites those + * URLs to `https://zenstack.dev` so search engines index the real domain. + * + * If anything goes wrong it returns `undefined`, falling through to the + * plain rewrite in `vercel.json` (unmodified page instead of an error). + */ + +const FRAMER_ORIGIN = 'https://zenstack.framer.website'; +const CANONICAL_ORIGIN = 'https://zenstack.dev'; + +export const config = { matcher: '/' }; + +export default async function middleware(request: Request): Promise { + try { + // preserve the original query string; forward only headers that may + // affect the HTML Framer serves (never cookie/host) + const { search } = new URL(request.url); + const requestHeaders = new Headers({ accept: 'text/html' }); + for (const name of ['user-agent', 'accept-language', 'referer', 'x-forwarded-for']) { + const value = request.headers.get(name); + if (value) { + requestHeaders.set(name, value); + } + } + + const upstream = await fetch(`${FRAMER_ORIGIN}/${search}`, { headers: requestHeaders }); + + const contentType = upstream.headers.get('content-type') ?? ''; + if (!upstream.ok || !contentType.includes('text/html')) { + return undefined; + } + + const html = await upstream.text(); + const rewritten = html.split(FRAMER_ORIGIN).join(CANONICAL_ORIGIN); + + const headers = new Headers(upstream.headers); + // the body was re-encoded, so the upstream encoding/length no longer apply + headers.delete('content-encoding'); + headers.delete('content-length'); + + return new Response(rewritten, { status: upstream.status, headers }); + } catch { + return undefined; + } +} From ff4723555187afbbc947603ae8cef4cad5715ec7 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 31 Jul 2026 02:46:17 -0700 Subject: [PATCH 14/16] rephrase text, add accessibility label --- docusaurus.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 11d88a34..c1659302 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -117,7 +117,8 @@ const config = { { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/zenstackhq/zenstack', - label: '⭐ GitHub', + label: '⭐ on GitHub', + 'aria-label': 'Star ZenStack on GitHub', position: 'right', className: 'bg-gray-900 rounded-sm', }, From 77eecabdd036d02b3e1c8f814883e551f79f6702 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 31 Jul 2026 03:27:26 -0700 Subject: [PATCH 15/16] Revert "docs: add PGlite section (#617)" This reverts commit 83e11082acda3e6fa01184eb95df8fdf7543efe1. --- blog/react-table/index.md | 2 +- docs/community-packages.md | 4 - docs/orm/api/omit.md | 6 +- docs/recipe/databases/postgres.md | 21 --- docs/reference/plugins/soft-delete.md | 152 ---------------------- docs/reference/zmodel/input-validation.md | 40 ------ docusaurus.config.js | 17 +-- middleware.ts | 50 ------- src/components/Sponsorship.tsx | 30 +++++ src/components/UserLogos.tsx | 79 +++++++++++ src/css/custom.css | 39 ++---- src/pages/_components/AICoding.tsx | 69 ++++++++++ src/pages/_components/Notes.tsx | 22 ++++ src/pages/_components/ORM.tsx | 62 +++++++++ src/pages/_components/Schema.tsx | 55 ++++++++ src/pages/_components/Service.tsx | 112 ++++++++++++++++ src/pages/_components/ValueProps.tsx | 56 ++++++++ src/pages/index.module.css | 24 ++++ src/pages/index.tsx | 123 +++++++++++++++++ static/img/new-logo-full.png | Bin 5324 -> 0 bytes static/img/new-logo.png | Bin 684 -> 0 bytes vercel.json | 8 +- 22 files changed, 658 insertions(+), 313 deletions(-) delete mode 100644 docs/reference/plugins/soft-delete.md delete mode 100644 middleware.ts create mode 100644 src/components/Sponsorship.tsx create mode 100644 src/components/UserLogos.tsx create mode 100644 src/pages/_components/AICoding.tsx create mode 100644 src/pages/_components/Notes.tsx create mode 100644 src/pages/_components/ORM.tsx create mode 100644 src/pages/_components/Schema.tsx create mode 100644 src/pages/_components/Service.tsx create mode 100644 src/pages/_components/ValueProps.tsx create mode 100644 src/pages/index.module.css create mode 100644 src/pages/index.tsx delete mode 100644 static/img/new-logo-full.png delete mode 100644 static/img/new-logo.png diff --git a/blog/react-table/index.md b/blog/react-table/index.md index b4416720..2ea5b81d 100644 --- a/blog/react-table/index.md +++ b/blog/react-table/index.md @@ -19,7 +19,7 @@ For two reasons, tables are one of the nastiest things to build in web UI. First > Building UI is a very branded and custom experience, even if that means choosing a design system or adhering to a design spec. - tanstack.com -Tables are most commonly used to render database query results — in modern times, the output of an ORM. In this post, I'll introduce a way of connecting [Prisma](https://prisma.io) - the most popular TypeScript ORM, to React Table, with the help of [React Query](https://tanstack.com/query) and [ZenStack](https://zenstack.dev). You'll be amazed by how little code you need to write to render a full-fledged table UI. +Tables are most commonly used to render database query results — in modern times, the output of an ORM. In this post, I'll introduce a way of connecting [Prisma](https://prisma.io) - the most popular TypeScript ORM, to React Table, with the help of [React Query](https://tanstack.com/query) and [ZenStack](/). You'll be amazed by how little code you need to write to render a full-fledged table UI. ## A full-stack setup diff --git a/docs/community-packages.md b/docs/community-packages.md index 211dac6b..fa2cfc89 100644 --- a/docs/community-packages.md +++ b/docs/community-packages.md @@ -7,10 +7,6 @@ sidebar_label: Community Packages ZenStack plugins built by our awesome community members :heart:. -- [zenstack-mcp](https://github.com/azzerty23/zenstack-mcp) - - Expose your ZenStack V3 database to LLMs through a [Model Context Protocol](https://modelcontextprotocol.io) interface, with built-in access-control policies and OAuth 2.0 authentication. Made by [Azzerty23](https://github.com/azzerty23). - - [zenstack-replicas](https://github.com/visualbravo/zenstack-replicas) Distribute read operations across database replicas with round-robin load balancing for the ZenStack ORM. Made by [@sanny-io](https://github.com/sanny-io). diff --git a/docs/orm/api/omit.md b/docs/orm/api/omit.md index 04e3b817..5baaf769 100644 --- a/docs/orm/api/omit.md +++ b/docs/orm/api/omit.md @@ -13,10 +13,6 @@ The previous sections have shown how you can use `select` and `omit` clauses to Please note that the omit settings only affect the ORM query APIs, but not the [Query Builder APIs](../query-builder). With query builder you'll need to explicitly specify the fields to select. ::: -:::warning -Omit (at all three layers) is not a security feature. It's merely a convenient way to control the default set of fields returned during a query. Don't rely on it to protect sensitive data — use [access policies](../access-control) for that purpose. -::: - ## Schema-Level Omit You can use the `@omit` attribute in ZModel to mark fields to be omitted. Such fields will be omitted by default for all `ZenStackClient` instances and all queries. @@ -75,7 +71,7 @@ const users = await db.user.findMany({ }); ``` -There might be scenarios where you don't want the query-level override feature. For example, when using `ZenStackClient` with the [Query as a Service](../../service), you may want to have certain fields always omitted. In such cases, use the `allowQueryTimeOmitOverride` option to disable query-time overrides: +There might be scenarios where you don't want the query-level override feature. For example, when using `ZenStackClient` with the [Query as a Service](../../service), you may want to have certain fields always omitted for security reasons. In such cases, use the `allowQueryTimeOmitOverride` option to disable query-time overrides: ```ts const db = new ZenStackClient({ diff --git a/docs/recipe/databases/postgres.md b/docs/recipe/databases/postgres.md index fbad0498..15c0de24 100644 --- a/docs/recipe/databases/postgres.md +++ b/docs/recipe/databases/postgres.md @@ -26,24 +26,3 @@ const db = new ZenStackClient(schema, { }), }); ``` - -## Using PGlite - -:::danger No Official Support -The PGlite dialect is not officially supported or tested by ZenStack, but you may evaluate it and [report your findings and interest on the GitHub issue](https://github.com/zenstackhq/zenstack/issues/2710). -::: - - - -```ts -import { schema } from './zenstack/schema'; -import { PGlite } from '@electric-sql/pglite'; -import { ZenStackClient } from '@zenstackhq/orm'; -import { PGliteDialect } from 'kysely'; - -const db = new ZenStackClient(schema, { - dialect: new PGliteDialect({ - pglite: new PGlite(), - }), -}); -``` diff --git a/docs/reference/plugins/soft-delete.md b/docs/reference/plugins/soft-delete.md deleted file mode 100644 index bf69c922..00000000 --- a/docs/reference/plugins/soft-delete.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -sidebar_position: 4 ---- - -import AvailableSince from '../../_components/AvailableSince'; -import PreviewFeature from '../../_components/PreviewFeature'; - -# @zenstackhq/plugin-soft-delete - - - - - -The `@zenstackhq/plugin-soft-delete` plugin implements **soft delete** by intercepting Kysely queries at runtime. Instead of physically removing rows, delete operations mark them with a timestamp, and reads automatically exclude the marked rows. - -## How It Works - -The plugin works off a single `@deletedAt` marker field on each model that should support soft deletion: - -- **Deletes become updates** — a `delete`/`deleteMany` against a soft-delete model is rewritten to set the `@deletedAt` field to the current timestamp instead of issuing a `DELETE`. -- **Reads are filtered** — `find*` queries (and joined relations) automatically add a ` IS NULL` condition, so soft-deleted rows are invisible. -- **Updates skip tombstones** — `update`/`updateMany` won't touch rows that are already soft-deleted. - -Models without a `@deletedAt` field are left completely untouched. - -## Installation - -```bash -npm install @zenstackhq/plugin-soft-delete -``` - -## Usage - -### 1. Declare the plugin in your ZModel schema - -Declaring the plugin makes the `@deletedAt` attribute available in your schema. - -```zmodel -plugin softDelete { - provider = '@zenstackhq/plugin-soft-delete' -} -``` - -### 2. Mark a nullable `DateTime` field with `@deletedAt` - -A model can have at most one `@deletedAt` field, and it must be optional (so that "not deleted" is represented by `null`). - -```zmodel -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] - deletedAt DateTime? @deletedAt -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int - deletedAt DateTime? @deletedAt -} -``` - -### 3. Install the plugin on your client at runtime - -```ts -import { ZenStackClient } from '@zenstackhq/orm'; -import { SoftDeletePlugin } from '@zenstackhq/plugin-soft-delete'; -import { schema } from './schema'; - -const db = new ZenStackClient(schema, { ... }).$use(new SoftDeletePlugin()); - -const user = await db.user.create({ data: { email: 'a@example.com' } }); - -// rewritten to set `deletedAt` — the row is kept in the database -await db.user.delete({ where: { id: user.id } }); - -// returns `null` — soft-deleted rows are hidden from reads -await db.user.findUnique({ where: { id: user.id } }); -``` - -### Works with the query builder APIs - -Because the plugin intercepts queries at the Kysely level, soft-delete behavior also applies to the low-level [query builder](../../orm/query-builder.md) escape hatch (`$qb`), not just the ORM API. Deletes are rewritten to `@deletedAt` updates and reads are filtered there too. - -```ts -// rewritten to set `deletedAt` instead of issuing a DELETE -await db.$qb.deleteFrom('User').where('id', '=', user.id).execute(); - -// only returns rows where `deletedAt IS NULL` -await db.$qb.selectFrom('User').selectAll().execute(); -``` - -## ZModel Declarations - -### Attributes - -#### `@deletedAt` - -```zmodel -attribute @deletedAt() -``` - -Marks the field used as the soft-delete tombstone marker. The field must be an optional `DateTime?`. A model may declare at most one `@deletedAt` field. - -## Caveats - -- **Soft deletes do not cascade.** Children of a soft-deleted parent are left untouched — managing them is up to you. (Note that a *hard* delete on a model without `@deletedAt` still triggers database-level `onDelete: Cascade` as usual.) -- **Multi-table / joined deletes can't be rewritten.** A joined or multi-table `DELETE` that targets a soft-delete model is rejected rather than silently hard-deleting rows. Use a single-table delete instead. -- **Unique constraints and tombstones.** Because soft-deleted rows physically remain, a plain `@unique` field will reject reusing a value held by a tombstone. See [Reusing unique values](#reusing-unique-values) below for the mitigation. - -## Reusing unique values - -A `@unique` field in ZModel compiles to a regular database unique constraint that also covers soft-deleted rows. So once a user with `email = "a@example.com"` is soft-deleted, you can't create another user with the same email — the tombstone still occupies that value. - -The fix is a **partial (filtered) unique index** scoped to live rows (`deletedAt IS NULL`). ZModel can't express this, so you add it through a **manually created migration**. Generate an empty migration with `--create-only`, then edit its SQL: - -```bash -npx zenstack migrate dev --create-only --name soft_delete_unique_email -``` - -In the generated migration, drop the plain unique constraint and replace it with a partial one. The exact SQL depends on your database: - -**PostgreSQL** — supports partial indexes directly: - -```sql -CREATE UNIQUE INDEX "User_email_active_key" - ON "User" ("email") - WHERE "deletedAt" IS NULL; -``` - -**SQLite** — also supports partial indexes: - -```sql -CREATE UNIQUE INDEX "User_email_active_key" - ON "User" ("email") - WHERE "deletedAt" IS NULL; -``` - -**MySQL** — has no partial indexes, but a unique index allows multiple `NULL`s, so use it over an expression that is the value only for live rows (and `NULL` for tombstones): - -```sql -ALTER TABLE `User` - ADD UNIQUE INDEX `User_email_active_key` ( - (CASE WHEN `deletedAt` IS NULL THEN `email` END) - ); -``` - -:::caution -Migrations are diff-based, so if you leave `@unique` on the field the next `migrate dev` will detect the plain index as "missing" and try to recreate it. To keep the schema and database in sync, drop `@unique` from the field in ZModel and let the manual partial index enforce uniqueness instead. -::: diff --git a/docs/reference/zmodel/input-validation.md b/docs/reference/zmodel/input-validation.md index 535fde14..71f8ca10 100644 --- a/docs/reference/zmodel/input-validation.md +++ b/docs/reference/zmodel/input-validation.md @@ -91,26 +91,6 @@ All field-level attributes have a `message` parameter that allows you to provide Requires a string field to be a valid ISO 8601 datetime. - - `@date` - - - - ```zmodel - @date(_ message: String?) - ``` - - Requires a string field to be a valid ISO 8601 date (e.g. `2024-01-31`). - - - `@time` - - - - ```zmodel - @time(_ precision: Int?, _ message: String?) - ``` - - Requires a string field to be a valid ISO 8601 time (e.g. `14:30:00`). The optional `precision` argument constrains the number of fractional-second digits allowed. - - `@regex` ```zmodel @@ -253,26 +233,6 @@ All field-level attributes have a `message` parameter that allows you to provide Checks if a string field is a valid ISO 8601 datetime. -- `isDate()` - - - - ```zmodel - function isDate(field: String): Boolean {} - ``` - - Checks if a string field is a valid ISO 8601 date. - -- `isTime()` - - - - ```zmodel - function isTime(field: String, precision: Int?): Boolean {} - ``` - - Checks if a string field is a valid ISO 8601 time. The optional `precision` argument constrains the number of fractional-second digits allowed. - - `regex()` ```zmodel diff --git a/docusaurus.config.js b/docusaurus.config.js index c1659302..ed2188a9 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -10,7 +10,7 @@ const config = { baseUrl: '/', onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', - favicon: 'img/new-logo.png', + favicon: 'img/logo.png', // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. @@ -25,13 +25,6 @@ const config = { locales: ['en'], }, - stylesheets: [ - { - href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap', - type: 'text/css', - }, - ], - presets: [ [ 'classic', @@ -98,9 +91,8 @@ const config = { title: 'ZenStack', logo: { alt: 'ZenStack Logo', - src: 'img/new-logo.png', - href: 'pathname:///', - target: '_self', + src: 'img/logo.png', + srcDark: 'img/logo-dark.png', }, items: [ { @@ -117,8 +109,7 @@ const config = { { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/zenstackhq/zenstack', - label: '⭐ on GitHub', - 'aria-label': 'Star ZenStack on GitHub', + label: '⭐ GitHub', position: 'right', className: 'bg-gray-900 rounded-sm', }, diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 43b89b42..00000000 --- a/middleware.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Vercel Edge Middleware. - * - * The `/` route is rewritten to the Framer-hosted landing page (see - * `vercel.json`), whose HTML declares `zenstack.framer.website` as the - * canonical/og:url. This middleware proxies the page and rewrites those - * URLs to `https://zenstack.dev` so search engines index the real domain. - * - * If anything goes wrong it returns `undefined`, falling through to the - * plain rewrite in `vercel.json` (unmodified page instead of an error). - */ - -const FRAMER_ORIGIN = 'https://zenstack.framer.website'; -const CANONICAL_ORIGIN = 'https://zenstack.dev'; - -export const config = { matcher: '/' }; - -export default async function middleware(request: Request): Promise { - try { - // preserve the original query string; forward only headers that may - // affect the HTML Framer serves (never cookie/host) - const { search } = new URL(request.url); - const requestHeaders = new Headers({ accept: 'text/html' }); - for (const name of ['user-agent', 'accept-language', 'referer', 'x-forwarded-for']) { - const value = request.headers.get(name); - if (value) { - requestHeaders.set(name, value); - } - } - - const upstream = await fetch(`${FRAMER_ORIGIN}/${search}`, { headers: requestHeaders }); - - const contentType = upstream.headers.get('content-type') ?? ''; - if (!upstream.ok || !contentType.includes('text/html')) { - return undefined; - } - - const html = await upstream.text(); - const rewritten = html.split(FRAMER_ORIGIN).join(CANONICAL_ORIGIN); - - const headers = new Headers(upstream.headers); - // the body was re-encoded, so the upstream encoding/length no longer apply - headers.delete('content-encoding'); - headers.delete('content-length'); - - return new Response(rewritten, { status: upstream.status, headers }); - } catch { - return undefined; - } -} diff --git a/src/components/Sponsorship.tsx b/src/components/Sponsorship.tsx new file mode 100644 index 00000000..71dc61f9 --- /dev/null +++ b/src/components/Sponsorship.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +export default function Sponsorship(): JSX.Element { + return ( +
+
+

Our Generous Sponsors

+
+ + + + +
+
+
+ ); +} + +function Sponsor({ src, name, website }: { src: string; name: string; website: string }): JSX.Element { + const alt = src.split('/').pop()?.split('.')[0] ?? 'logo'; + return ( + + {alt} +

{name}

+
+ ); +} diff --git a/src/components/UserLogos.tsx b/src/components/UserLogos.tsx new file mode 100644 index 00000000..d9068967 --- /dev/null +++ b/src/components/UserLogos.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +interface UserLogoProps { + src: string; + name: string; + website: string; + className?: string; + style?: React.CSSProperties; + imageStyle?: React.CSSProperties; + darkSrc?: string; +} + +function UserLogo({ src, name, website, className, style, imageStyle, darkSrc }: UserLogoProps): JSX.Element { + return ( +
+ {name} + {name} + + {name} + +
+ ); +} + +export default function UserLogs(): JSX.Element { + return ( +
+
+

Used and Loved by

+
+ + + + + + + +
+
+
+ ); +} diff --git a/src/css/custom.css b/src/css/custom.css index 96fdf67d..c6dfd23d 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -10,40 +10,29 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #ff3f02; - --ifm-color-primary-dark: #e63902; - --ifm-color-primary-darker: #cc3202; - --ifm-color-primary-darkest: #b32c01; - --ifm-color-primary-light: #ff521b; - --ifm-color-primary-lighter: #ff6535; - --ifm-color-primary-lightest: #ff794e; - --ifm-font-family-base: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, - 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --ifm-color-primary: #ff7100; + --ifm-color-primary-dark: #e66600; + --ifm-color-primary-darker: #cc5a00; + --ifm-color-primary-darkest: #b34f00; + --ifm-color-primary-light: #ff7f1a; + --ifm-color-primary-lighter: #ff8d33; + --ifm-color-primary-lightest: #ff9c4d; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 148, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { - --ifm-color-primary: #ff521b; - --ifm-color-primary-dark: #ff3f02; - --ifm-color-primary-darker: #e63902; - --ifm-color-primary-darkest: #cc3202; - --ifm-color-primary-light: #ff6535; - --ifm-color-primary-lighter: #ff794e; - --ifm-color-primary-lightest: #ff8c67; + --ifm-color-primary: #ff7f1a; + --ifm-color-primary-dark: #ff7100; + --ifm-color-primary-darker: #e66600; + --ifm-color-primary-darkest: #cc5a00; + --ifm-color-primary-light: #ff8d33; + --ifm-color-primary-lighter: #ff9c4d; + --ifm-color-primary-lightest: #ffaa66; --docusaurus-highlighted-code-line-bg: rgba(0, 148, 0, 0.3); } -.navbar__logo { - margin-right: 0.125rem; -} - -.navbar__title { - color: var(--ifm-color-primary); - text-transform: uppercase; -} - .footer { @apply px-8 lg:px-16; } diff --git a/src/pages/_components/AICoding.tsx b/src/pages/_components/AICoding.tsx new file mode 100644 index 00000000..aad73f88 --- /dev/null +++ b/src/pages/_components/AICoding.tsx @@ -0,0 +1,69 @@ +type FeatureItem = { + title: string; + img: string; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Single Source of Truth', + img: '/img/access-control.png', + description: ( + <> + When LLMs see a self-contained, non-ambiguous, and well-defined application model, their inference works + more efficiently and effectively. + + ), + }, + { + title: 'Concise Query API', + img: '/img/auto-api.png', + description: ( + <> + Concise and expressive, while leveraging existing knowledge of Prisma and Kysely, the query API makes it + easy for LLMs to generate high-quality query code. + + ), + }, + { + title: 'Slim Code Base', + img: '/img/ai-friendly.png', + description: ( + <> + By deriving artifacts from the schema instead of implementing them, ZenStack helps you maintain a slim + code base that is easier for AI to digest. + + ), + }, +]; + +function Proposition({ title, img, description }: FeatureItem) { + return ( +
+
+ {title} +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function AICoding(): JSX.Element { + return ( +
+
+

+ Perfect Match for AI-Assisted Programming +

+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+ ); +} diff --git a/src/pages/_components/Notes.tsx b/src/pages/_components/Notes.tsx new file mode 100644 index 00000000..6bcfbb83 --- /dev/null +++ b/src/pages/_components/Notes.tsx @@ -0,0 +1,22 @@ +import Link from '@docusaurus/Link'; + +export default function Notes(): JSX.Element { + return ( +
+
+

+ Notes to V2 Users +

+
+
+
+ ZenStack V3 has made the bold decision to remove Prisma as a runtime dependency and implement its + own ORM infrastructure on top of Kysely. Albeit the cost of such a + big refactor, we believe this is the right move to gain the flexibility needed to achieve the + project's vision. Please read this blog post for more + thoughts behind the changes. +
+
+
+ ); +} diff --git a/src/pages/_components/ORM.tsx b/src/pages/_components/ORM.tsx new file mode 100644 index 00000000..2d9bcfdb --- /dev/null +++ b/src/pages/_components/ORM.tsx @@ -0,0 +1,62 @@ +import CodeBlock from '@theme/CodeBlock'; + +export default function ORM(): JSX.Element { + return ( +
+
+

+
Flexible and Awesomely Typed ORM
+

+
+
+ + {`import { schema } from './zenstack'; +import { ZenStackClient } from '@zenstackhq/orm'; +import { PolicyPlugin } from '@zenstackhq/plugin-policy'; + +const db = new ZenStackClient(schema, { ... }) + // install access control plugin to enforce policies + .$use(new PolicyPlugin()) + // set current user context + .$setAuth(...); + +// high-level query API +const userWithPosts = await db.user.findUnique({ + where: { id: userId }, + include: { posts: true } +}); + +// low-level SQL query builder API +const userPostJoin = await db + .$qb + .selectFrom('User') + .innerJoin('Post', 'Post.authorId', 'User.id') + .select(['User.id', 'User.email', 'Post.title']) + .where('User.id', '=', userId) + .execute(); +`} + +
+

+ An ORM is derived from the schema that gives you +

+
    +
  • 🔋 High-level ORM query API
  • +
  • 🔋 Low-level SQL query builder API
  • +
  • 🔋 Access control enforcement
  • +
  • 🔋 Runtime data validation
  • +
  • 🔋 Computed fields and custom procedures
  • +
  • 🔋 Plugin system for tapping into various lifecycle events
  • +
+ + ZenStack's ORM is built on top of the awesome Kysely SQL query + builder. Its query API is compatible with that of{' '} + Prisma Client, so migrating an + existing Prisma project will require minimal code changes.{' '} + Read more about migrating from Prisma. + +
+
+
+ ); +} diff --git a/src/pages/_components/Schema.tsx b/src/pages/_components/Schema.tsx new file mode 100644 index 00000000..04fc4a59 --- /dev/null +++ b/src/pages/_components/Schema.tsx @@ -0,0 +1,55 @@ +import CodeBlock from '@theme/CodeBlock'; + +export default function SchemaLanguage(): JSX.Element { + return ( +
+
+

+
Intuitive and Expressive Data Modeling
+

+
+
+
+

The modeling language allows you to

+
    +
  • ✅ Define data models and relations
  • +
  • ✅ Define access control policies
  • +
  • ✅ Express data validation rules
  • +
  • ✅ Model polymorphic inheritance
  • +
  • ✅ Add custom attributes and functions to introduce custom semantics
  • +
  • ✅ Implement custom code generators
  • +
+ + The schema language is a superset of{' '} + Prisma Schema Language. + Migrating a Prisma schema is as simple as file renaming. + +
+ + {`model User { + id Int @id + email String @unique @email // constraint and validation + role String + posts Post[] // relation to another model + postCount Int @computed // computed field + + // access control rules colocated with data + @@allow('all', auth().id == id) + @@allow('create, read', true) +} + +model Post { + id Int @id + title String @length(1, 255) + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int // relation foreign key + + @@allow('read', published) + @@allow('all', auth().id == authorId || auth().role == 'ADMIN') +}`} + +
+
+ ); +} diff --git a/src/pages/_components/Service.tsx b/src/pages/_components/Service.tsx new file mode 100644 index 00000000..73b1975c --- /dev/null +++ b/src/pages/_components/Service.tsx @@ -0,0 +1,112 @@ +import CodeBlock from '@theme/CodeBlock'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +export default function Service(): JSX.Element { + return ( +
+
+

+
Automatic HTTP Query Service
{' '} +

+
+
+
+

+ Thanks to the ORM's built-in access control, you get an HTTP query service for free +

+
    +
  • 🚀 Fully mirrors the ORM API
  • +
  • 🚀 Seamlessly integrates with popular frameworks
  • +
  • 🚀 Works with any authentication solution
  • +
  • + 🚀 Type-safe client SDK powered by{' '} + + TanStack Query + +
  • +
  • 🚀 Highly customizable
  • +
+
+

+ Since the ORM is protected with access control, ZenStack can directly map it to an HTTP + service. ZenStack provides out-of-the-box integrations with popular frameworks including + Next.js, Nuxt, Express, etc. +

+

+ Client hooks based on{' '} + + TanStack Query + {' '} + can also be derived from the schema, allowing you to make type-safe queries to the service + without writing a single line of code. +

+
+
+
+ + + + {`import { NextRequestHandler } from '@zenstackhq/server/next'; +import { db } from './db'; // ZenStackClient instance +import { getSessionUser } from './auth'; + +// callback to provide a per-request ORM client +async function getClient() { + // call a framework-specific helper to get session user + const authUser = await getSessionUser(); + + // return a new ORM client configured with the user, + // the user info will be used to enforce access control + return db.$setAuth(authUser); +} + +// Create a request handler for all requests to this route +// All CRUD requests are forwarded to the underlying ORM +const handler = NextRequestHandler({ getClient }); + +export { + handler as GET, + handler as PUT, + handler as POST, + handler as PATCH, + handler as DELETE, +}; + `} + + + + + {`import { schema } from './zenstack'; +import { useClientQueries } from '@zenstackhq/tanstack-query/react'; + +export function UserPosts({ userId }: { userId: number }) { + // use auto-generated hook to query user with posts + const client = useClientQueries(schema); + const { data, isLoading } = client.user.useFindUnique({ + where: { id: userId }, + include: { posts: true } + }); + + if (isLoading) return
Loading...
; + + return ( +
+

{data?.email}'s Posts

+
    + {data?.posts.map((post) => ( +
  • {post.title}
  • + ))} +
+
+ ); +} + `} +
+
+
+
+
+
+ ); +} diff --git a/src/pages/_components/ValueProps.tsx b/src/pages/_components/ValueProps.tsx new file mode 100644 index 00000000..555ecdad --- /dev/null +++ b/src/pages/_components/ValueProps.tsx @@ -0,0 +1,56 @@ +type FeatureItem = { + title: string; + img: string; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Coherent Schema', + img: '/img/diagram.png', + description: ( + <> + Simple schema language to capture the most important aspects of your application in one place: data and + security. + + ), + }, + { + title: 'Powerful ORM', + img: '/img/search.png', + description: ( + <>One-of-a-kind ORM that combines type-safety, query flexibility, and access control in one package. + ), + }, + { + title: 'Limitless Utility', + img: '/img/versatility.png', + description: ( + <>Deriving crucial artifacts that streamline development from backend APIs to frontend components. + ), + }, +]; + +function Proposition({ title, img, description }: FeatureItem) { + return ( +
+
+ {title} +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function ValueProps(): JSX.Element { + return ( +
+ {FeatureList.map((props, idx) => ( + + ))} +
+ ); +} diff --git a/src/pages/index.module.css b/src/pages/index.module.css new file mode 100644 index 00000000..b3332046 --- /dev/null +++ b/src/pages/index.module.css @@ -0,0 +1,24 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 8rem 2rem !important; + text-align: center !important; + position: relative !important; + overflow: hidden !important; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding-top: 4rem !important; + padding-bottom: 4rem !important; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 00000000..36f6fc32 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,123 @@ +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import clsx from 'clsx'; +import React from 'react'; +import Sponsorship from '../components/Sponsorship'; +import UserLogs from '../components/UserLogos'; +import AICoding from './_components/AICoding'; +import ORM from './_components/ORM'; +import SchemaLanguage from './_components/Schema'; +import Service from './_components/Service'; +import ValueProps from './_components/ValueProps'; +import styles from './index.module.css'; + +const description = `ZenStack is a powerful data layer for modern TypeScript applications. It provides an intuitive data modeling language, a fully type-safe ORM, built-in access control and data validation, and automatic data query service that seamlessly integrates with popular frameworks like Next.js and Nuxt.`; + +function Header() { + return ( +
+
+
+
+

+ + Modern Data Layer for TypeScript Applications + +

+

+ Intuitive data modeling, type-safe ORM, built-in access control, automatic query services, + and more. +

+
+ + Get Started → + + + Open Playground + +
+
+
+
+
+ ); +} + +function Section({ children, className }: { children: React.ReactNode; className?: string }) { + return ( +
+
{children}
+
+ ); +} + +export default function Home(): JSX.Element { + const { siteConfig } = useDocusaurusContext(); + return ( + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + Start Building Now → + +
+
+ +
+ +
+ + Support this project → + +
+
+ + {/*
+ +
*/} +
+ + ); +} diff --git a/static/img/new-logo-full.png b/static/img/new-logo-full.png deleted file mode 100644 index c47e5b6bc747694c37fcc98d44cd4fcb29279529..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5324 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 QvJb@dboFyt=akR{0D-V`7XSbN diff --git a/vercel.json b/vercel.json index 7b1be7fe..0bec30f8 100644 --- a/vercel.json +++ b/vercel.json @@ -2,8 +2,12 @@ "trailingSlash": false, "rewrites": [ { - "source": "/", - "destination": "https://zenstack.framer.website/" + "source": "/framer", + "destination": "https://detailed-costs-055147.framer.app/" + }, + { + "source": "/framer/:path*", + "destination": "https://detailed-costs-055147.framer.app/:path*" } ], "redirects": [ From 7567795cfc3431f24b81f4bb0b327cb15b3a5d11 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 31 Jul 2026 03:30:14 -0700 Subject: [PATCH 16/16] rephrase text, add accessibility label --- docusaurus.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index ed2188a9..56845980 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -109,7 +109,8 @@ const config = { { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/zenstackhq/zenstack', - label: '⭐ GitHub', + label: '⭐ on GitHub', + 'aria-label': 'Star ZenStack on GitHub', position: 'right', className: 'bg-gray-900 rounded-sm', },