diff --git a/docusaurus.config.js b/docusaurus.config.js
index 0dd2e501..5eef4d59 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -44,6 +44,10 @@ const config = {
                             label: '1.x',
                             banner: 'none',
                         },
+                        '3.x': {
+                            label: '3.0 Alpha',
+                            banner: 'none',
+                        },
                     },
                 },
                 blog: false,
diff --git a/package.json b/package.json
index fa70489e..d5b8de74 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,10 @@
         "@docusaurus/theme-mermaid": "3.4.0",
         "@giscus/react": "^2.4.0",
         "@mdx-js/react": "^3.0.1",
+        "@stackblitz/sdk": "^1.11.0",
         "autoprefixer": "^10.4.13",
         "clsx": "^1.2.1",
+        "is-mobile": "^5.0.0",
         "postcss": "^8.4.21",
         "prism-react-renderer": "^2.3.1",
         "prism-svelte": "^0.5.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 78043273..ec59cc8f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,12 +29,18 @@ importers:
       '@mdx-js/react':
         specifier: ^3.0.1
         version: 3.0.1(@types/react@18.0.26)(react@18.2.0)
+      '@stackblitz/sdk':
+        specifier: ^1.11.0
+        version: 1.11.0
       autoprefixer:
         specifier: ^10.4.13
         version: 10.4.13(postcss@8.4.21)
       clsx:
         specifier: ^1.2.1
         version: 1.2.1
+      is-mobile:
+        specifier: ^5.0.0
+        version: 5.0.0
       postcss:
         specifier: ^8.4.21
         version: 8.4.21
@@ -1113,6 +1119,9 @@ packages:
   '@slorber/remark-comment@1.0.0':
     resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==}
 
+  '@stackblitz/sdk@1.11.0':
+    resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==}
+
   '@svgr/babel-plugin-add-jsx-attribute@8.0.0':
     resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
     engines: {node: '>=14'}
@@ -3010,6 +3019,9 @@ packages:
     resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
     engines: {node: '>=10'}
 
+  is-mobile@5.0.0:
+    resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==}
+
   is-npm@6.0.0:
     resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -6914,6 +6926,8 @@ snapshots:
       micromark-util-character: 1.2.0
       micromark-util-symbol: 1.1.0
 
+  '@stackblitz/sdk@1.11.0': {}
+
   '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.23.9)':
     dependencies:
       '@babel/core': 7.23.9
@@ -9091,6 +9105,8 @@ snapshots:
       global-dirs: 3.0.1
       is-path-inside: 3.0.3
 
+  is-mobile@5.0.0: {}
+
   is-npm@6.0.0: {}
 
   is-number@7.0.0: {}
diff --git a/src/components/GithubCodeBlock.tsx b/src/components/GithubCodeBlock.tsx
new file mode 100644
index 00000000..5ab8770a
--- /dev/null
+++ b/src/components/GithubCodeBlock.tsx
@@ -0,0 +1,40 @@
+import CodeBlock from '@theme/CodeBlock';
+import { useEffect, useState } from 'react';
+
+interface GithubCodeBlockProps {
+    repoPath: string;
+    file: string;
+}
+
+const GithubCodeBlock: React.FC<GithubCodeBlockProps> = ({ repoPath, file }) => {
+    const [code, setCode] = useState<string>('Loading...');
+
+    useEffect(() => {
+        (async function () {
+            const response = await fetch(`https://cdn.jsdelivr.net/gh/${repoPath}/${file}`);
+            if (!response.ok) {
+                setCode(`Unable to load "${repoPath}/${file}"`);
+                return;
+            }
+            const text = await response.text();
+            setCode(text);
+        })();
+    }, [repoPath, file]);
+
+    const getLanguage = (file: string): string => {
+        if (file.endsWith('.ts')) {
+            return 'typescript';
+        } else if (file.endsWith('.zmodel')) {
+            return 'zmodel';
+        } else {
+            return 'plaintext';
+        }
+    };
+    return (
+        <CodeBlock language={getLanguage(file)} title={file}>
+            {code}
+        </CodeBlock>
+    );
+};
+
+export default GithubCodeBlock;
diff --git a/src/components/StackBlitzEmbed.tsx b/src/components/StackBlitzEmbed.tsx
new file mode 100644
index 00000000..db7b6418
--- /dev/null
+++ b/src/components/StackBlitzEmbed.tsx
@@ -0,0 +1,26 @@
+import React, { useEffect, useRef } from 'react';
+import sdk from '@stackblitz/sdk';
+
+interface StackBlitzEmbedProps {
+    projectId: string;
+    height?: string;
+}
+
+const StackBlitzEmbed: React.FC<StackBlitzEmbedProps> = ({ projectId, height = '600px' }) => {
+    const containerRef = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+        if (containerRef.current) {
+            sdk.embedProjectId(containerRef.current, projectId, {
+                openFile: 'main.ts',
+                height,
+                view: 'editor',
+                forceEmbedLayout: true,
+            });
+        }
+    }, [projectId, height]);
+
+    return <div ref={containerRef} style={{ width: '100%', height }} />;
+};
+
+export default StackBlitzEmbed;
diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx
new file mode 100644
index 00000000..052aff98
--- /dev/null
+++ b/src/components/StackBlitzGithubEmbed.tsx
@@ -0,0 +1,84 @@
+import sdk from '@stackblitz/sdk';
+import TabItem from '@theme/TabItem';
+import Tabs from '@theme/Tabs';
+import mobile from 'is-mobile';
+import React, { useEffect, useRef } from 'react';
+import GithubCodeBlock from './GithubCodeBlock';
+
+interface StackBlitzGithubEmbedProps {
+    repoPath: string;
+    height?: string;
+    openFile?: string;
+    plainCodeFiles?: string[];
+    startScript?: string;
+    clickToLoad?: boolean;
+}
+
+const StackBlitzGithubEmbed: React.FC<StackBlitzGithubEmbedProps> = ({
+    repoPath,
+    height = '600px',
+    openFile = 'main.ts',
+    plainCodeFiles = undefined,
+    clickToLoad = false,
+    startScript,
+}) => {
+    const containerRef = useRef<HTMLDivElement>(null);
+
+    const options = {
+        openFile,
+        height,
+        view: 'editor',
+        startScript,
+        clickToLoad,
+    } as const;
+
+    if (!plainCodeFiles) {
+        plainCodeFiles = [openFile];
+    }
+
+    useEffect(() => {
+        if (containerRef.current) {
+            setTimeout(() => {
+                // docusaurus seems to recreate the tab after mounting, give it a
+                // chance to complete
+                sdk.embedGithubProject(containerRef.current, repoPath, options);
+            }, 0);
+        }
+    }, [repoPath, height, containerRef]);
+
+    const PlainCode = () => (
+        <>
+            {plainCodeFiles.map((file) => (
+                <GithubCodeBlock key={file} repoPath={repoPath} file={file} />
+            ))}
+        </>
+    );
+
+    if (mobile()) {
+        return <PlainCode />;
+    } else {
+        return (
+            <>
+                <Tabs groupId="interactive-sample">
+                    <TabItem value="static" label="Plain Code" default>
+                        <PlainCode />
+                    </TabItem>
+                    <TabItem value="interactive" label="Interactive Sample">
+                        <div className="italic text-sm mb-1">
+                            Click{' '}
+                            <a href="#" onClick={() => sdk.openGithubProject(repoPath, options)}>
+                                here
+                            </a>{' '}
+                            to pop out if the embed doesn't load an interactive terminal.
+                        </div>
+                        <div ref={containerRef} style={{ width: '100%', height }} />
+                    </TabItem>
+                </Tabs>
+
+                <div></div>
+            </>
+        );
+    }
+};
+
+export default StackBlitzGithubEmbed;
diff --git a/versioned_docs/version-3.x/_components/PackageExec.tsx b/versioned_docs/version-3.x/_components/PackageExec.tsx
new file mode 100644
index 00000000..e3df2110
--- /dev/null
+++ b/versioned_docs/version-3.x/_components/PackageExec.tsx
@@ -0,0 +1,28 @@
+import CodeBlock from '@theme/CodeBlock';
+import TabItem from '@theme/TabItem';
+import Tabs from '@theme/Tabs';
+
+interface Props {
+    command: string;
+}
+
+const pkgManagers = [
+    { name: 'npm', command: 'npx' },
+    { name: 'pnpm', command: 'pnpm' },
+    { name: 'bun', command: 'bunx' },
+    { name: 'yarn', command: 'npx' },
+];
+
+const PackageInstall = ({ command }: Props) => {
+    return (
+        <Tabs>
+            {pkgManagers.map((pkg) => (
+                <TabItem key={pkg.name} value={pkg.name} label={pkg.name}>
+                    <CodeBlock language="bash">{`${pkg.command} ${command}`}</CodeBlock>
+                </TabItem>
+            ))}
+        </Tabs>
+    );
+};
+
+export default PackageInstall;
diff --git a/versioned_docs/version-3.x/_components/PackageInstall.tsx b/versioned_docs/version-3.x/_components/PackageInstall.tsx
new file mode 100644
index 00000000..53bf8805
--- /dev/null
+++ b/versioned_docs/version-3.x/_components/PackageInstall.tsx
@@ -0,0 +1,33 @@
+import CodeBlock from '@theme/CodeBlock';
+import TabItem from '@theme/TabItem';
+import Tabs from '@theme/Tabs';
+
+interface Props {
+    devDependencies: string[];
+    dependencies: string[];
+}
+
+const pkgManagers = [
+    { name: 'npm', command: 'npm install', dev: '--save-dev' },
+    { name: 'pnpm', command: 'pnpm add', dev: '--save-dev' },
+    { name: 'bun', command: 'bun add', dev: '--dev' },
+    { name: 'yarn', command: 'yarn add', dev: '--dev' },
+];
+
+const PackageInstall = ({ devDependencies, dependencies }: Props) => {
+    return (
+        <Tabs>
+            {pkgManagers.map((pkg) => (
+                <TabItem key={pkg.name} value={pkg.name} label={pkg.name}>
+                    <CodeBlock language="bash">
+                        {`${devDependencies?.length ? `${pkg.command} ${pkg.dev} ${devDependencies.join(' ')}\n` : ''}${
+                            dependencies?.length ? `${pkg.command} ${dependencies.join(' ')}` : ''
+                        }`}
+                    </CodeBlock>
+                </TabItem>
+            ))}
+        </Tabs>
+    );
+};
+
+export default PackageInstall;
diff --git a/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx
new file mode 100644
index 00000000..46b7a50d
--- /dev/null
+++ b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx
@@ -0,0 +1,16 @@
+import React, { FC } from 'react';
+import Admonition from '@theme/Admonition';
+
+interface ZModelVsPSLProps {
+    children: React.ReactNode;
+}
+
+const ZModelVsPSL: FC<ZModelVsPSLProps> = ({ children }) => {
+    return (
+        <Admonition type="info" title="🔋 ZModel vs Prisma Schema">
+            {children}
+        </Admonition>
+    );
+};
+
+export default ZModelVsPSL;
diff --git a/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx b/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx
new file mode 100644
index 00000000..323adb57
--- /dev/null
+++ b/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx
@@ -0,0 +1,16 @@
+import React, { FC } from 'react';
+import Admonition from '@theme/Admonition';
+
+interface ZenStackVsPrismaProps {
+    children: React.ReactNode;
+}
+
+const ZenStackVsPrisma: FC<ZenStackVsPrismaProps> = ({ children }) => {
+    return (
+        <Admonition type="info" title="🔋 ZenStack vs Prisma">
+            {children}
+        </Admonition>
+    );
+};
+
+export default ZenStackVsPrisma;
diff --git a/versioned_docs/version-3.x/_components/_zmodel-starter.md b/versioned_docs/version-3.x/_components/_zmodel-starter.md
new file mode 100644
index 00000000..fbf3eaea
--- /dev/null
+++ b/versioned_docs/version-3.x/_components/_zmodel-starter.md
@@ -0,0 +1,23 @@
+  ```zmodel
+  datasource db {
+    provider = 'sqlite'
+    url = "file:./dev.db"
+  }
+
+  model User {
+    id       String @id @default(cuid())
+    email    String @unique
+    posts    Post[]
+  }
+
+  model Post {
+    id        String   @id @default(cuid())
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+    title     String
+    content   String
+    published Boolean  @default(false)
+    author    User     @relation(fields: [authorId], references: [id])
+    authorId  String
+  }
+  ```
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/faq.md b/versioned_docs/version-3.x/faq.md
new file mode 100644
index 00000000..0a788729
--- /dev/null
+++ b/versioned_docs/version-3.x/faq.md
@@ -0,0 +1,14 @@
+---
+description: ZenStack FAQ.
+
+slug: /faq
+sidebar_label: FAQ
+sidebar_position: 100
+---
+
+# 🙋🏻 FAQ
+
+## What databases are supported?
+
+Currently only SQLite (with [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) or [sql.js](https://github.com/sql-js/sql.js) driver) and PostgreSQL (with [node-postgres](https://github.com/brianc/node-postgres) driver) are supported. MySQL will be added in the future. There's no plan to support other relational databases or NoSQL databases.
+
diff --git a/versioned_docs/version-3.x/migrate-prisma.md b/versioned_docs/version-3.x/migrate-prisma.md
new file mode 100644
index 00000000..c5310bb6
--- /dev/null
+++ b/versioned_docs/version-3.x/migrate-prisma.md
@@ -0,0 +1,261 @@
+---
+description: How to migrate from a Prisma project to ZenStack v3
+sidebar_position: 10
+---
+
+import PackageInstall from './_components/PackageInstall';
+import TabItem from '@theme/TabItem';
+import Tabs from '@theme/Tabs';
+
+# Migrating From Prisma
+
+## Overview
+
+This guide will help you migrate an existing Prisma project to ZenStack v3. The process is made straightforward by the following design decisions:
+
+1. The ZModel schema language is a superset of Prisma Schema Language.
+2. The ORM provides a Prisma-compatible API.
+3. The migration engine is built on top of Prisma Migrate.
+
+In the following sections, we'll cover various aspects of the migration process where care needs to be taken.
+
+:::warning
+ZenStack v3 currently only supports PostgreSQL and SQLite databases.
+:::
+
+## Migration Steps
+
+### 1. Project dependencies
+
+ZenStack v3 doesn't depend on Prisma at runtime. Its CLI has a peer dependency to the `prisma` package for migration related commands. The most straightforward way is to follow these steps:
+
+- Remove `prisma` and `@prisma/client` from your project dependencies.
+- Install ZenStack packages
+    
+    <PackageInstall dependencies={['@zenstackhq/runtime@next']} devDependencies={['@zenstackhq/cli@next']} />
+
+- Install database driver
+  
+    ZenStack doesn't bundle database drivers. Install the appropriate driver for your database:
+
+    <Tabs>
+    <TabItem value="postgres" label="PostgreSQL">
+
+    <PackageInstall dependencies={['pg']} devDependencies={['@types/pg']} />
+
+    </TabItem>
+    <TabItem value="sqlite" label="SQLite">
+
+    <PackageInstall dependencies={['better-sqlite3']} devDependencies={['@types/better-sqlite3']} />
+
+    </TabItem>
+    </Tabs>
+
+
+You don't need to explicitly install `prisma` package because the `@zenstackhq/cli` has a peer dependency on it.
+
+### 2. Migrate your schema
+
+If you have a single `schema.prisma` file, you can move and rename it to `zenstack/schema.zmodel`. No change should be necessary because every valid Prisma schema is also a valid ZModel schema. The only optional change you can consider to make is to remove the Prisma client generator block since it doesn't have any effect in ZenStack:
+
+```zmodel
+generator client {
+  provider = "prisma-client-js"
+}
+```
+
+If you use Prisma's [multi-schema feature](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema), you'll need to explicitly use the `import` statement to merge related schema file into a whole graph. See [Multi-file Schema](./modeling/multi-file.md) for details.
+
+### 3. Update generation command
+
+In your `package.json` scripts, replace the `prisma generate` command with `zen generate`.
+
+```json
+{
+  "scripts": {
+    "generate": "zen generate"
+  }
+}
+```
+
+### 4. Update database client instantiation
+
+Replace `new PrismaClient()` with `new ZenStackClient(schema, ...)` where `schema` is imported from the TypeScript code generated by the `zen generate` command.
+
+<Tabs>
+
+<TabItem value="postgres" label={`PostgreSQL`}>
+
+```ts title='db.ts'
+import { ZenStackClient } from '@zenstackhq/runtime';
+import { schema } from '@/zenstack/schema';
+import { PostgresDialect } from 'kysely';
+import { Pool } from 'pg';
+
+export const db = new ZenStackClient(schema, {
+  dialect: new PostgresDialect({
+    pool: new Pool({
+      connectionString: process.env.DATABASE_URL,
+    }),
+  }),
+});
+```
+</TabItem>
+
+<TabItem value="sqlite" label={`SQLite`}>
+
+```ts title='db.ts'
+import { ZenStackClient } from '@zenstackhq/runtime';
+import { SqliteDialect } from 'kysely';
+import SQLite from 'better-sqlite3';
+import { schema } from '@/zenstack/schema';
+
+export const db = new ZenStackClient(schema, {
+  dialect: new SqliteDialect({
+    database: new SQLite('./db.sqlite'),
+  }),
+});
+```
+</TabItem>
+
+</Tabs>
+
+Since `ZenStackClient` has a PrismaClient-compatible API, you should not need to change your existing code that uses the database client.
+
+### 5. Update type references
+
+Prisma generates many TypeScript types that you may have used in your code, such as `User`, `UserCreateArgs`, etc. ZenStack replicates some of the most useful types in its generated code.
+
+For top-level model types like `User`, you can import from the `models.ts` file. For other detailed input types like `UserCreateArgs`, import them from the `input.ts` file.
+
+### 6. Update migration command
+
+In your `package.json` scripts, replace the Prisma `db` and `migrate` commands with ZenStack equivalents:
+
+```json
+{
+  "scripts": {
+    "db:push": "zen db push",
+    "migrate:dev": "zen migrate dev",
+    "migrate:deploy": "zen migrate deploy"
+  }
+}
+```
+
+## Other Considerations
+
+Now let's check the areas less straightforward to migrate.
+
+### Prisma custom generators
+
+ZenStack has its own plugin system and doesn't support Prisma custom generators. However, you can continue running them with the following two steps:
+
+1. Use the [@core/prisma](./reference/plugins/prisma.md) plugin to generate a Prisma schema from ZModel.
+   
+    ```zmodel
+    plugin prisma {
+      provider = '@core/prisma'
+      output = './schema.prisma'
+    }
+    ```
+
+2. Run a`prisma generate` command after `zen generate` with the prisma schema as input.
+   
+   ```json
+   {
+     "scripts": {
+       "generate": "zen generate && prisma generate --schema=zenstack/schema.prisma"
+     }
+   }
+   ```
+
+### Prisma client extensions
+
+ZenStack has its own [runtime plugin mechanism](./orm/plugins/) and doesn't plan to be compatible with Prisma client extensions. However, there are easy migration paths for the two most popular types of Prisma client extensions.
+
+**1. Query extension**
+
+[Query extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/query) allows you to intercepts ORM query calls.
+
+Suppose you have an extension like:
+
+```ts
+const extPrisma = prisma.$extends({
+  query: {
+    user: {
+      async findMany({ model, operation, args, query }) {
+        // take incoming `where` and set `age`
+        args.where = { ...args.where, age: { gt: 18 } }
+        return query(args)
+      },
+    },
+  },
+});
+```
+
+You can replace it with an equivalent ZenStack plugin:
+
+```ts
+const extDb = db.$use({
+  id: 'my-plugin',
+  onQuery: {
+    user: {
+      async findMany({ model, operation, args, proceed }) {
+        // take incoming `where` and set `age`
+        args.where = { ...args.where, age: { gt: 18 } }
+        return proceed(args)
+      },
+    },
+  },
+});
+```
+
+You can also use the special `$allModels` and `$allOperations` keys to apply the plugin to all models and operations like when using Prisma client extensions.
+
+**2. Result extension**
+
+[Result extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/result) allows you to add custom fields to query results. ZenStack provides a mechanism that achieves the same goal but a lot more powerful.
+
+Suppose you have a result extension like:
+
+```ts
+const extPrisma = prisma.$extends({
+  result: {
+    user: {
+      fullName: {
+        // the dependencies
+        needs: { firstName: true, lastName: true },
+        compute(user) {
+          // the computation logic
+          return `${user.firstName} ${user.lastName}`
+        },
+      },
+    },
+  },
+});
+```
+
+You can replace it with a ZenStack computed fields with changes in ZModel and database client instantiation.
+
+```zmodel title="zenstack/schema.zmodel"
+model User {
+  ...
+  firstName String
+  lastName  String
+  fullName  String  @computed
+}
+```
+
+```ts
+export const db = new ZenStackClient(schema, {
+  ...,
+  computedFields: {
+    User: {
+      // SQL: CONCAT(firstName, ' ', lastName)
+      fullName: (eb) => eb.fn('concat', ['firstName', eb.val(' '), 'lastName'])
+    },
+  },
+});
+```
+
+The biggest difference is ZenStack's computed fields are evaluated on the database side - much more efficient and flexible than client-side computation. Read more in the [Computed Fields](./orm/computed-fields.md) documentation.
diff --git a/versioned_docs/version-3.x/modeling/_category_.yml b/versioned_docs/version-3.x/modeling/_category_.yml
new file mode 100644
index 00000000..2df4bdd6
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/_category_.yml
@@ -0,0 +1,4 @@
+position: 2
+label: Data Modeling
+collapsible: true
+collapsed: true
diff --git a/versioned_docs/version-3.x/modeling/attribute.md b/versioned_docs/version-3.x/modeling/attribute.md
new file mode 100644
index 00000000..df89825e
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/attribute.md
@@ -0,0 +1,47 @@
+---
+sidebar_position: 4
+description: Attributes in ZModel
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Attribute
+
+Attributes allow you to attach metadata to models and fields. As you've seen in the previous sections, they are used for many purposes, such as adding unique constraints and mapping names. Attributes are also indispensable for modeling relations between models.
+
+## Naming conventions
+
+By convention, attributes attached to models use a double `@@` prefix, while those for fields use a single `@` prefix.
+
+```zmodel
+model User {
+    id        Int    @id
+    email     String @unique
+
+    @@index([email, name])
+}
+```
+
+## Defining and applying attributes
+
+<ZModelVsPSL>
+Prisma schema doesn't allow users to define custom attributes, while ZModel allows it and uses it as a key mechanism for extensibility.
+</ZModelVsPSL>
+
+ZModel comes with a rich set of attributes that you can use directly. See [ZModel Language Reference](../reference/zmodel-language.md) for a complete list. You can also define your own custom attributes for specific purposes. Attributes are defined with a list of typed parameters. Parameters can be named (default) or positional. Positional parameters can be passed with or without an explicit name. Parameters can also be optional.
+
+Here's an example of how the `@unique` attribute is defined:
+
+```zmodel
+attribute @unique(map: String?, length: Int?, sort: SortOrder?)
+```
+
+You can apply it in various ways:
+
+```zmodel
+model Foo {
+    x String @unique() // default application
+    y String @unique('y_unique') // positional parameter
+    z String @unique(map: 'z_unique', length: 10) // named parameter
+}
+```
diff --git a/versioned_docs/version-3.x/modeling/conclusion.md b/versioned_docs/version-3.x/modeling/conclusion.md
new file mode 100644
index 00000000..06890e4a
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/conclusion.md
@@ -0,0 +1,16 @@
+---
+sidebar_position: 100
+description: Data modeling conclusion
+---
+
+# Conclusion
+
+Congratulations! You have learned all the essentials of data model with ZenStack. What's next?
+
+In the following parts, you'll learn how to put the schema into use and let it drive many aspects of your application, including:
+
+- [Using ORM to query the database](../orm/)
+- [Exposing a data API with Query-as-a-Service](../service/)
+- [Deriving other useful utilities](../service/)
+
+Let's continue our journey with ZenStack!
diff --git a/versioned_docs/version-3.x/modeling/custom-type.md b/versioned_docs/version-3.x/modeling/custom-type.md
new file mode 100644
index 00000000..46f966ea
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/custom-type.md
@@ -0,0 +1,46 @@
+---
+sidebar_position: 7
+description: Custom types in ZModel
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Custom Type
+
+<ZModelVsPSL>
+Custom type is a ZModel concept and doesn't exist in PSL.
+</ZModelVsPSL>
+
+Besides models, you can also define custom types to encapsulate complex data structures. The main difference between a model and a custom type is that the latter is not backed by a database table.
+
+Here's a simple example:
+
+```zmodel
+type Address {
+    street  String
+    city    String
+    country String
+    zip     Int
+}
+```
+
+Custom types are defined exactly like models, with the exception that they cannot contain fields that are relations to other models. They can, however, contain fields that are other custom types.
+
+There are two ways to use custom types:
+
+```zmodel
+type Address {
+    street  String
+    city    String
+    country String
+    zip     Int
+}
+
+type UserProfile {
+    gender  String
+    address Address?
+}
+```
+
+- [Mixin](./mixin.md)
+- [Strongly typed JSON fields](./strong-typed-json.md)
diff --git a/versioned_docs/version-3.x/modeling/datasource.md b/versioned_docs/version-3.x/modeling/datasource.md
new file mode 100644
index 00000000..2ac7a052
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/datasource.md
@@ -0,0 +1,42 @@
+---
+sidebar_position: 2
+description: Datasource in ZModel
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Data Source
+
+The `datasource` block provides information about the database your application uses. The ORM relies on it to determine the proper SQL dialect to use when generating queries. If you use [Migration](../orm/migration.md), it must also have a `url` field that specifies the database connection string, so that the migration engine knows how to connect to the database. The `env` function can be used to reference environment variables so you can keep sensitive information out of the code.
+
+Each ZModel schema must have exactly one `datasource` block.
+
+<Tabs>
+
+<TabItem value="postgresql" label="PostgreSQL" default>
+```zmodel
+datasource db {
+    provider = 'postgresql'
+    url      = env('DATABASE_URL')
+}
+```
+</TabItem>
+
+<TabItem value="sqlite" label="SQLite">
+```zmodel
+datasource db {
+    provider = 'sqlite'
+    url      = 'file:./dev.db'
+}
+```
+</TabItem>
+
+</Tabs>
+
+Currently, only PostgreSQL and SQLite are supported. MySQL will be supported in a future release. There's no plan for other relational database types or NoSQL databases.
+
+<ZModelVsPSL>
+ZenStack's ORM runtime doesn't rely on the `url` information to connect to the database. Instead, you provide the information when constructing an ORM client — more on this in the [ORM](../orm/) section.
+</ZModelVsPSL>
diff --git a/versioned_docs/version-3.x/modeling/enum.md b/versioned_docs/version-3.x/modeling/enum.md
new file mode 100644
index 00000000..f6410bba
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/enum.md
@@ -0,0 +1,27 @@
+---
+sidebar_position: 4
+description: Enums in ZModel
+---
+
+# Enum
+
+Enums are simple constructs that allow you to define a set of named values.
+
+```zmodel
+enum Role {
+    USER
+    ADMIN
+}
+```
+
+They can be used to type model fields:
+
+```zmodel
+model User {
+    id   Int  @id
+    // highlight-next-line
+    role Role @default(USER)
+}
+```
+
+Enum field names are added to the global scope and are resolved without qualification. You need to make sure they don't collide with other global names.
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/modeling/index.md b/versioned_docs/version-3.x/modeling/index.md
new file mode 100644
index 00000000..2f7d5cf7
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/index.md
@@ -0,0 +1,52 @@
+---
+sidebar_position: 1
+description: ZModel overview
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Data Modeling Overview
+
+ZenStack uses a schema language named **ZModel** to define data models and their related aspects. We know that designing a good schema language is difficult, and we know it's even more challenging to convince people to learn a new one. We therefore decided to design ZModel as a superset of the [Prisma Schema Language (PSL)](https://www.prisma.io/docs/orm/prisma-schema), one of the best data modeling languages available.
+
+If you're already familiar with PSL, you'll find yourself at home with ZModel. However, we recommend that you skim through this section to learn about the essential extensions we made to PSL. Please pay attention to callouts like the following one:
+
+<ZModelVsPSL>
+ZModel allows both single quotes and double quotes for string literals.
+</ZModelVsPSL>
+
+Don't worry if you've never used Prisma before. This section will introduce all aspects of ZModel, so no prior knowledge is required.
+
+A simplest ZModel schema looks like this:
+
+```zmodel title='zenstack/schema.zmodel'
+datasource db {
+    provider = "postgresql"
+    url      = env("DATABASE_URL")
+}
+
+model User {
+    id    Int    @id @default(autoincrement())
+    email String @unique
+    name  String
+}
+
+model Post {
+    id    Int    @id @default(autoincrement())
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+    title     String
+    content   String
+    published Boolean  @default(false)
+    author    User     @relation(fields: [authorId], references: [id])
+    authorId  String
+}
+```
+
+<ZModelVsPSL>
+Prisma has the concept of "generator", which provides a pluggable mechanism to generate artifacts from PSL. Specifically, you need to define a "prisma-client-js" (or "prisma-client") generator to get the ORM client.
+
+ZenStack CLI generates a TypeScript schema without needing any configuration. Also, it replaced PSL's "generator" with a more generalized "plugin" construct that allows you to extend the system both at the schema level and the runtime level. Read more in the [Plugin](./plugin) section.
+</ZModelVsPSL>
+
+Let's dissect it piece by piece.
diff --git a/versioned_docs/version-3.x/modeling/mixin.md b/versioned_docs/version-3.x/modeling/mixin.md
new file mode 100644
index 00000000..c08b2b95
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/mixin.md
@@ -0,0 +1,47 @@
+---
+sidebar_position: 8
+description: Reusing common fields with mixins
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Mixin
+
+<ZModelVsPSL>
+Mixin is a ZModel concept and don't exist in PSL.
+</ZModelVsPSL>
+
+:::info
+Mixin was previously known as "abstract inheritance" in ZenStack v2. It's renamed and changed to use the `with` keyword to distinguish from polymorphic model inheritance.
+:::
+
+Very often you'll find many of your models share quite a few common fields. It's tedious and error-prone to repeat them. As a rescue, you can put those fields into custom types, and "mix-in" them into your models.
+
+***Before:***
+
+```zmodel
+model User {
+    id        String @id
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+    email     String @unique
+}
+```
+
+***After:***
+
+```zmodel
+type BaseFieldsMixin {
+    id        String   @id
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+}
+
+model User with BaseFieldsMixin {
+    email String @unique
+}
+```
+
+A model can use multiple mixins as long as their field names don't conflict.
+
+Mixins don't exist at the database level. The fields defined in the mixin types are conceptually inlined into the models that use them.
diff --git a/versioned_docs/version-3.x/modeling/model.md b/versioned_docs/version-3.x/modeling/model.md
new file mode 100644
index 00000000..0442bf5b
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/model.md
@@ -0,0 +1,182 @@
+---
+sidebar_position: 3
+description: Models in ZModel
+---
+
+# Model
+
+The `model` construct is the core of ZModel. It defines the structure of your data and relations. A model represents a domain entity and is mapped to a database table.
+
+## Defining models
+
+A typical model looks like this:
+
+```zmodel
+model User {
+    id        Int      @id @default(autoincrement())
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+    email     String   @unique
+    name      String
+}
+```
+
+The simplest models are just a collection of fields. A model must be uniquely identifiable by some of its fields. For most cases, you'll have a field marked with the `@id` attribute (more about [attributes](./attribute) later). 
+
+```zmodel
+model User {
+    // highlight-next-line
+    id Int @id
+}
+```
+
+If your model needs a composite ID, you can use the `@@id` model-level attribute to specify it:
+
+```zmodel
+model City {
+    country String
+    name String
+    // highlight-next-line
+    @@id([country, name])
+}
+```
+
+If no `@id` or `@@id` is specified, the ORM will resort to using a field (or fields) marked with the `@unique` or `@@unique` attribute as the identifier.
+
+```zmodel
+model User {
+    // highlight-next-line
+    email String @unique
+}
+
+model City {
+    country String
+    name String
+    // highlight-next-line
+    @@unique([country, name])
+}
+```
+
+## Model fields
+
+Each model field must at least have a name and a type. A field can be typed in one of the following ways:
+
+1. Built-in types, including:
+   - String
+   - Boolean
+   - Int
+   - BigInt
+   - Float
+   - Decimal
+   - DateTime
+   - Json
+   - Bytes
+   - Unsupported
+  
+   The `Unsupported` type is for defining fields of types not supported by the ORM; however, it lets the migration engine know how to create the field in the database.
+
+   ```zmodel
+   // from Prisma docs
+   model Star {
+       id       Int                    @id
+       // highlight-next-line
+       position Unsupported("circle")? @default(dbgenerated("'<(10,4),11>'::circle"))
+   }
+   ```
+
+2. Enum
+   
+   We'll talk about [enums](./enum) later.
+
+   ```zmodel
+   enum Role {
+       USER
+       ADMIN
+   }
+
+   model User {
+       id Int @id
+       // highlight-next-line
+       role Role
+   }
+   ```
+
+3. Model
+    
+    It'll then form a relation. We'll cover that topic [later](./relation).
+
+    ```zmodel
+    model Post {
+        id      Int @id
+        // highlight-next-line
+        author  User   @relation(fields: [authorId], references: [id])
+        authorId Int
+    }
+    ```
+4. Custom type
+   
+   ZenStack allows you to define custom types in the schema and use them to type JSON fields. This will be covered in more detail in the [Custom Types](./custom-type) section.
+
+   ```zmodel
+   type Address {
+       street  String
+       city    String
+       country String
+       zip     Int
+   }
+
+   model User {
+       id Int  @id
+       // highlight-next-line
+       address Address @json
+   }
+   ```
+
+A field can be set as optional by adding the `?` suffix to its type, or list by adding the `[]` suffix. However, a field cannot be both optional and a list at the same time.
+
+```zmodel
+model User {
+    id   Int     @id
+    // highlight-next-line
+    name String?
+    // highlight-next-line
+    tags String[]
+}
+```
+
+A default value can be specified for a field with the `@default` attribute. The value can be a literal, or a supported function call, including:
+
+- `now()`: returns the current timestamp
+- `cuid()`: returns a CUID
+- `uuid()`: returns a UUID
+- `ulid()`: returns a ULID
+- `nanoid()`: returns a Nano ID
+- `autoincrement()`: returns an auto-incrementing integer (only for integer fields)
+- `dbgenerated("...")`: calls a native db function
+
+```zmodel
+model User {
+    id        Int  @id @default(autoincrement())
+    role      Role @default("USER")
+    createdAt DateTime @default(now())
+}
+```
+
+## Native type mapping
+
+Besides giving a field a type, you can also specify the native database type to use with the `@db.` series of attributes.
+
+```zmodel
+model User {
+    ...
+    // highlight-next-line
+    name String @db.VarChar(64)
+}
+```
+
+These attributes control what data type is used when the [migration engine](../orm/migration.md) maps the schema to DDL. You can find a complete list of native type attributes in the [ZModel Language Reference](../reference/zmodel-language).
+
+## Name mapping
+
+Quite often, you want to use a different naming scheme for your models and fields than the database. You can achieve that with the `@map` attribute. The ORM respects the mapping when generating queries, and the migration engine uses it to generate the DDL.
+
diff --git a/versioned_docs/version-3.x/modeling/multi-file.md b/versioned_docs/version-3.x/modeling/multi-file.md
new file mode 100644
index 00000000..75aaeb26
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/multi-file.md
@@ -0,0 +1,42 @@
+---
+sidebar_position: 11
+description: Breaking down complex schemas into multiple files
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Multi-file Schema
+
+<ZModelVsPSL>
+Prisma uses an implicit approach that simply merges all schema files in a folder. ZModel uses explicit `import` syntax for better clarity and flexibility.
+</ZModelVsPSL>
+
+When your schema grows large, you can break them down to smaller files and stitch them together using the `import` statement.
+
+```zmodel title="zenstack/user.zmodel"
+import './post'
+
+model User {
+    id    Int   @id
+    posts Post[]
+}
+```
+
+```zmodel title="zenstack/post.zmodel"
+import './user'
+
+model Post {
+    id       Int    @id
+    content  String
+    author   User   @relation(fields: [authorId], references: [id])
+    authorId Int
+}
+```
+
+```zmodel title="zenstack/schema.zmodel"
+import './user'
+import './post'
+```
+
+After type-checking, these files are merged into a single schema AST before passed to the downstream tools.
+
diff --git a/versioned_docs/version-3.x/modeling/plugin.md b/versioned_docs/version-3.x/modeling/plugin.md
new file mode 100644
index 00000000..ca8f9589
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/plugin.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 11
+description: ZenStack plugins
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Plugin
+
+<ZModelVsPSL>
+ZenStack's "plugin" concept replaces PSL's "generator".
+</ZModelVsPSL>
+
+Plugin is a powerful mechanism that allows you to extend ZenStack at the schema, CLI, and runtime levels. This section only focuses on how to add plugins to your ZModel. Please refer to the [Plugin Development](../reference/plugin-dev.md) section for more details on how to develop plugins.
+
+## Adding plugins to ZModel
+
+Let's take a look at the following example:
+
+```zmodel
+plugin myPlugin {
+    provider = 'my-zenstack-plugin'
+    output = './generated'
+}
+```
+
+:::info
+In fact, the `zen generate` command is entirely implemented with plugins. The ZModel -> TypeScript generation is supported by the built-in `@core/typescript` plugin, which can be explicitly declared if you wish:
+
+```zmodel
+plugin typescript {
+  provider = '@core/typescript'
+  output = '../generated'
+}
+```
+
+Please refer to the [Plugin References](../category/plugins) for the full list of built-in plugins.
+:::
+
+A plugin declaration involves three parts:
+
+1. A unique name
+2. A `provider` field that specifies where to load the plugin from. It can be a built-in plugin (like `@core/prisma` here), a local folder, or an npm package.
+3. Plugin-specific configuration options, such as `output` in this case.
+
+A plugin can have the following effects to ZModel:
+
+- It can contribute custom attributes that you can use to annotate models and fields.
+- It can contribute code generation logic that's executed when you run the `zenstack generate` command.
+
+Plugins can also contribute to the ORM runtime behavior, and we'll leave it to the [ORM](../orm/plugins/) part to explain it in detail.
diff --git a/versioned_docs/version-3.x/modeling/polymorphism.md b/versioned_docs/version-3.x/modeling/polymorphism.md
new file mode 100644
index 00000000..98d78b71
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/polymorphism.md
@@ -0,0 +1,122 @@
+---
+sidebar_position: 10
+description: Polymorphic models in ZModel
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Polymorphism
+
+<ZModelVsPSL>
+Polymorphism is a ZModel feature and doesn't exist in PSL.
+</ZModelVsPSL>
+
+## Introduction
+
+When modeling non-trivial applications, the need of an "Object-Oriented" kind of polymorphism often arises:
+- Something **IS-A** more abstract type of thing.
+- Something **HAS-A/HAS-many** a more abstract type of thing(s).
+
+Imagine we're modeling a content library system where users own different types of content: posts, images, videos, etc. They share some common traits like name, creation date, owner, etc., but have different specific fields.
+
+It may be tempting to use mixins to share the common fields, however it's not an ideal solution because:
+
+- The `User` table will have relations to each of the content types.
+- There's no efficient and clean way to query all content types together (e.g., all content owned by a user).
+- Consequently, whenever you add a new content type, you'll need to modify the `User` model, and probably lots of query code too.
+
+A true solution involves having a in-database model of polymorphism, where we really have a `Content` table that serves as an intermediary between `User` and the concrete content types. This is what ZModel polymorphism is about.
+
+:::info
+There are [two main ways](https://www.prisma.io/docs/orm/prisma-schema/data-model/table-inheritance) to model polymorphism in relational databases: single-table inheritance (STI) and multi-table inheritance (MTI, aka. delegate types). ZModel only supports MTI.
+:::
+
+## Modeling polymorphism
+
+Modeling polymorphism in ZModel is similar to designing an OOP class hierarchy - you introduce a base model and then extend it with concrete ones.
+
+Here's how it looks for our content library example:
+
+```zmodel
+model User {
+    id       Int       @id
+    contents Content[]
+}
+
+model Content {
+    id        Int      @id
+    name      String
+    createdAt DateTime @default(now())
+    owner     User     @relation(fields: [ownerId], references: [id])
+    ownerId   Int
+    // highlight-next-line
+    type      String
+
+    // highlight-next-line
+    @@delegate(type)
+}
+
+model Post extends Content {
+    content String
+}
+
+model Image extends Content {
+    data Bytes
+}
+
+model Video extends Content {
+    url String
+}
+```
+
+```mermaid
+erDiagram
+	User {
+        id Int PK
+	}
+    Content {
+        id Int PK
+		name String
+        createdAt Date
+        ownerId Int FK
+        type String
+    }
+    User ||--o{ Content: owns
+    Post {
+        id Int PK
+        content String
+    }
+    Post ||--|| Content: delegates
+    Image {
+        id Int PK
+        data Bytes
+    }
+    Image ||--|| Content: delegates
+    Video {
+        id Int PK
+        url String
+    }
+    Video ||--|| Content: delegates
+```
+
+There are two special things about polymorphic base model:
+
+1. It must have a "discriminator" field that stores the concrete model type that it should "delegate" to. In the example above, the `type` field serves this purpose. It can be named anything you like, but must be of `String` or enum type.
+2. It must have a `@@delegate` attribute. The attribute serves two purposes: it indicates that the model is a base model, and it designates the discriminator field with its parameter.
+
+You can also have a deep hierarchy involving multiple level of base models. Just need to make sure each base model has its own discriminator field and `@@delegate` attribute. Extending from multiple base models directly is not supported.
+
+## Migration behavior
+
+The migration engine takes care of mapping both the base model and the concrete ones to tables, and creates one-to-one relations between the base and each of its derivations.
+
+To simplify query and conserve space, the base and the concrete are assumed to share the same id values (this is guaranteed by the ORM when creating the records), and consequently, the concrete model's id field is also reused as the foreign key to the base model. So, for a `Post` record with id `1`, the base `Content` record also has id `1`.
+
+## ORM behavior
+
+The ORM hides the delegate complexities and provides a simple polymorphic view to the developers:
+
+1. Creating a concrete model record automatically creates the base model record with the same id and proper discriminator field.
+2. Querying with the base model will return entities with concrete model fields.
+
+We'll revisit the topic in details in the [ORM](../orm/polymorphism.md) part.
diff --git a/versioned_docs/version-3.x/modeling/relation.md b/versioned_docs/version-3.x/modeling/relation.md
new file mode 100644
index 00000000..d0b023b9
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/relation.md
@@ -0,0 +1,219 @@
+---
+sidebar_position: 5
+description: Relations in ZModel
+---
+
+# Relation
+
+Relation is a fundamental concept in relational databases. It lets you connect models into a graph, and allows you to query interconnected data efficiently. In ZModel, relations are modeled using the `@relation` attribute. For most cases, it involves one side of the relation defining a foreign key field that references the primary key of the other side. By convention, we call the model that holds the foreign key the "owner" side.
+
+## One-to-one relation
+
+A typical one-to-one relation looks like this:
+
+```zmodel
+model User {
+    id      Int      @id
+    profile Profile?
+}
+
+model Profile {
+    id     Int  @id
+    user   User @relation(fields: [userId], references: [id])
+    userId Int  @unique
+}
+```
+
+The `Profile` model holds the foreign key `userId` and is the owner of the relation. The pk-fk association is established by the `@relation` attribute, where the `fields` parameter specifies the foreign key field(s) and the `references` parameter specifies the primary key field(s) of the other side.
+
+In one-to-one relations, the "non-owner" side must declare the relation field as optional (here `User.profile`), because there's no way to guarantee a `User` row always has a corresponding `Profile` row at the database level. The owner side can be either optional or required.
+
+Relations can also be explicitly named, and it's useful to disambiguate relations when a model has multiple relations to the same model, or to control the constraint name generated by the migration engine.
+
+```zmodel
+model User {
+    id        Int      @id
+    profile   Profile? @relation('UserProfile')
+}
+
+model Profile {
+    id        Int  @id
+    user      User @relation('UserProfile', fields: [userId], references: [id])
+    userId    Int  @unique
+}
+```
+
+Please note that even though both sides of the relation now have the `@relation` attribute, only the owner side can have the `fields` and `references` parameters.
+
+If a relation involves a model with composite PK fields, the FK fields must match the PK fields' count and types, and the `fields` and `references` parameters must be specified with those field tuples with matching order.
+
+```zmodel
+model User {
+    id1     Int
+    id2     Int
+    profile Profile?
+
+    @@id([id1, id2])
+}
+
+model Profile {
+    id      Int  @id
+    user    User @relation(fields: [userId1, userId2], references: [id1, id2])
+    userId1 Int
+    userId2 Int
+}
+```
+
+## One-to-many relation
+
+A typical one-to-many relation looks like this:
+
+```zmodel
+model User {
+    id        Int    @id
+    posts     Post[]
+}
+
+model Post {
+    id        Int  @id
+    author    User @relation(fields: [authorId], references: [id])
+    authorId  Int
+}
+```
+
+It's modeled pretty much the same way as one-to-one relations, except that the "non-owner" side (here `User.posts`) is a list of the other side's model type.
+
+## Many-to-many relation
+
+Many-to-many relations are modeled in the database through a join table, which forms a many-to-one relation with each of the two sides.
+
+In ZModel, there are two ways to model many-to-many relations: implicitly or explicitly.
+
+### Implicit many-to-many
+
+An implicit many-to-many relation simply defines both sides of the relation as lists of the other side's model type, without modeling a join table explicitly.
+
+```zmodel
+model User {
+    id    Int    @id
+    posts Post[]
+}
+
+model Post {
+    id      Int    @id
+    editors User[]
+}
+```
+
+Under the hood, the migration engine creates a join table named `_PostToUser` (model names are sorted alphabetically), and the ORM runtime transparently handles the join table for you.
+
+You can also name the join table explicitly by adding the `@relation` attribute to both sides:
+
+```zmodel
+model User {
+    id    Int    @id
+    posts Post[] @relation('UserPosts')
+}
+
+model Post {
+    id      Int    @id
+    editors User[] @relation('UserPosts')
+}
+```
+
+### Explicit many-to-many
+
+Explicit many-to-many relations are nothing but a join table with foreign keys linking the two sides.
+
+```zmodel
+model User {
+    id    Int        @id
+    posts UserPost[]
+}
+
+model Post {
+    id      Int        @id
+    editors UserPost[]
+}
+
+model UserPost {
+    userId Int
+    postId Int
+    user   User @relation(fields: [userId], references: [id])
+    post   Post @relation(fields: [postId], references: [id])
+
+    @@id([userId, postId])
+}
+```
+
+Since the join table is explicitly defined, when using the ORM, you'll need to involve it in your queries with an extra level of nesting.
+
+## Self relation
+
+Self-relations are cases where a model has a relation to itself. They can be one-to-one, one-to-many, or many-to-many.
+
+### One-to-one
+
+```zmodel
+model Employee {
+    id       Int       @id
+    mentorId Int?      @unique
+    mentor   Employee? @relation('Mentorship',   fields: [mentorId], references: [id])
+    mentee   Employee? @relation('Mentorship')
+}
+```
+
+Quick notes:
+
+- Both sides of the relation are defined in the same model.
+- Both relation fields need to have `@relation` attributes with matching names.
+- One side (here `mentor`) has a foreign key field (`mentorId`) that references the primary key.
+- The foreign key field is marked `@unique` to guarantee one-to-one.
+
+### One-to-many
+
+```zmodel
+model Employee {
+  id           Int        @id
+  managerId    Int
+  manager      Employee   @relation('Management', fields: [managerId], references: [id])
+  subordinates Employee[] @relation('Management')
+}
+```
+
+Quick notes:
+- Both sides of the relation are defined in the same model.
+- Both relation fields need to have `@relation` attributes with matching names.
+- One side (here `manager`) has a foreign key field (`managerId`) that references the primary key.
+- The owner side (`Employee.manager`) can be either optional or required based on your needs.
+
+### Many-to-many
+
+Defining an implicit many-to-many self-relation is very straightforward.
+
+```zmodel
+model Employee {
+  id      Int        @id
+  mentors Employee[] @relation('Mentorship')
+  mentees Employee[] @relation('Mentorship')
+}
+```
+
+You can also define an explicit one by modeling the join table explicitly.
+
+```zmodel
+model Employee {
+  id      Int          @id
+  mentors Mentorship[] @relation('Mentorship')
+  mentees Mentorship[] @relation('Mentorship')
+}
+
+model Mentorship {
+  mentorId Int
+  menteeId Int
+  mentor   Employee @relation('Mentorship', fields: [mentorId], references: [id])
+  mentee   Employee @relation('Mentorship', fields: [menteeId], references: [id])
+
+  @@id([mentorId, menteeId])
+}
+```
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/modeling/strong-typed-json.md b/versioned_docs/version-3.x/modeling/strong-typed-json.md
new file mode 100644
index 00000000..2426d926
--- /dev/null
+++ b/versioned_docs/version-3.x/modeling/strong-typed-json.md
@@ -0,0 +1,50 @@
+---
+sidebar_position: 9
+description: Strongly typed JSON fields
+---
+
+import ZModelVsPSL from '../_components/ZModelVsPSL';
+
+# Strongly Typed JSON
+
+<ZModelVsPSL>
+Strongly typed JSON is a ZModel feature and doesn't exist in PSL.
+</ZModelVsPSL>
+
+With relational databases providing better and better JSON support, their usage has become more common. However, in many cases your JSON fields still follow a specific structure, and when so, you can make the fields strongly typed so that:
+
+- When mutating the field, its structure is validated.
+- When querying the field, its result is strongly typed.
+
+To type a JSON field, define a custom type in ZModel, use it as the field's type, and additionally mark the field with the `@json` attribute.
+
+***Before:***
+
+```zmodel
+model User {
+    id      Int @id
+    address Json
+}
+```
+
+***After:***
+
+```zmodel
+type Address {
+    street  String
+    city    String
+    country String
+    zip     Int
+}
+
+model User {
+    id      Int     @id
+    address Address @json
+}
+```
+
+:::info
+The `@json` attribute doesn't do anything except to clearly mark the field is a JSON field but not a relation to another model.
+:::
+
+The migration engine still sees the field as a plain Json field. However, the ORM client enforces its structure and takes care of properly typing the query results. We'll revisit the topic in the [ORM](../orm/) part.
diff --git a/versioned_docs/version-3.x/orm/_category_.yml b/versioned_docs/version-3.x/orm/_category_.yml
new file mode 100644
index 00000000..6f69cf50
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/_category_.yml
@@ -0,0 +1,4 @@
+position: 3
+label: ORM
+collapsible: true
+collapsed: true
diff --git a/versioned_docs/version-3.x/orm/access-control/index.md b/versioned_docs/version-3.x/orm/access-control/index.md
new file mode 100644
index 00000000..0b584167
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/access-control/index.md
@@ -0,0 +1,7 @@
+---
+sidebar_position: 6
+---
+
+# Access Control 🚧
+
+Coming soon 🚧
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/orm/api/_select-include-omit.md b/versioned_docs/version-3.x/orm/api/_select-include-omit.md
new file mode 100644
index 00000000..5b57c5f4
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/_select-include-omit.md
@@ -0,0 +1,18 @@
+- `select`
+
+    An object specifying the fields to include in the result. Setting a field to `true` means to include it. If a field is a relation, you can provide an nested object to further specify which fields of the relation to include.
+
+    This field is optional. If not provided, all non-relation fields are included by default. The `include` field is mutually exclusive with the `select` field.
+
+- `include`
+
+    An object specifying the relations to include in the result. Setting a relation to `true` means to include it. You can pass an object to further choose what fields/relations are included for the relation, and/or a `where` clause to filter the included relation records.
+
+    This field is optional. If not provided, no relations are included by default. The `include` field is mutually exclusive with the `select` field.
+
+- `omit`
+
+    An object specifying the fields to omit from the result. Setting a field to `true` means to omit it. Only applicable to non-relation fields.
+
+    This field is optional. If not provided, no fields are omitted by default. The `omit` field is mutually exclusive with the `select` field.
+    
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/orm/api/aggregate.md b/versioned_docs/version-3.x/orm/api/aggregate.md
new file mode 100644
index 00000000..27b95a31
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/aggregate.md
@@ -0,0 +1,22 @@
+---
+sidebar_position: 7
+description: Aggregate API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Aggregate
+
+The `aggregate` method allows you to conduct multiple aggregations on a set of records with one operation. The supported aggregations are:
+
+- `_count` - equivalent to the [Count API](./count.md).
+- `_sum` - sum of a numeric field.
+- `_avg` - average of a numeric field.
+- `_min` - minimum value of a field.
+- `_max` - maximum value of a field.
+
+You can also use `where`, `orderBy`, `skip`, and `take` to control what records are included in the aggregation.
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-aggregate" openFile="main.ts" startScript="generate,dev" />
diff --git a/versioned_docs/version-3.x/orm/api/count.md b/versioned_docs/version-3.x/orm/api/count.md
new file mode 100644
index 00000000..d337e632
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/count.md
@@ -0,0 +1,14 @@
+---
+sidebar_position: 6
+description: Count API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Count
+
+You can use the `count` method to count the number of records that match a query. It also allows to count non-null field values with an `select` clause.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-count" openFile="main.ts" startScript="generate,dev" />
+
+To count relations, please use a `find` API with the special `_count` field as demonstrated in the [Find](./find.md#field-selection) section.
diff --git a/versioned_docs/version-3.x/orm/api/create.md b/versioned_docs/version-3.x/orm/api/create.md
new file mode 100644
index 00000000..5135ed23
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/create.md
@@ -0,0 +1,22 @@
+---
+sidebar_position: 2
+description: Create API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Create
+
+The `create` series of APIs are used to create new records in the database. It has the following methods:
+
+- `create`
+    Create a single record, optionally with nested relations.
+- `createMany`
+    Create multiple records in a single operation. Nested relations are not supported. Only the number of records created is returned.
+- `createManyAndReturn`
+    Similar to `createMany`, but returns the created records.
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="create.ts" startScript="generate,create" />
+
diff --git a/versioned_docs/version-3.x/orm/api/delete.md b/versioned_docs/version-3.x/orm/api/delete.md
new file mode 100644
index 00000000..e16e4dda
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/delete.md
@@ -0,0 +1,20 @@
+---
+sidebar_position: 5
+description: Delete API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Delete
+
+Deleting records can be done with the following methods:
+
+- `delete` - Delete a single, unique record.
+- `deleteMany` - Delete multiple records that match the query criteria.
+- `deleteManyAndReturn` - Similar to `deleteMany`, but returns the deleted records
+
+You can also delete records as part of an `update` operation from a relation. See [Manipulating relations](./update.md#manipulating-relations) for details.
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-delete" openFile="main.ts" startScript="generate,main" />
diff --git a/versioned_docs/version-3.x/orm/api/filter.md b/versioned_docs/version-3.x/orm/api/filter.md
new file mode 100644
index 00000000..53ac20d7
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/filter.md
@@ -0,0 +1,86 @@
+---
+sidebar_position: 3
+description: how to filter entities
+---
+
+import ZenStackVsPrisma from '../../_components/ZenStackVsPrisma';
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Filter
+
+Filtering is an important topic because it's involved in many ORM operations, for example when you find records, selecting relations, and updating or deleting multiple records.
+
+## Basic filters
+
+You can filter on scalar fields with values or operators as supported by the field type. The following filter operators are available.
+
+- `equals` `not`: all scalar fields.
+- `in` `notIn`: all scalar fields
+- `contains` `startsWith` `endsWith`: String fields
+- `lt` `lte` `gt` `gte`: String, Int, BigInt, Float, Decimal, and Date fields
+
+A filter object can contain multiple field filters, and they are combined with `AND` semantic. You can also use the `AND`, `OR`, and `NOT` logical operators to combine filter objects to form a complex filter.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="filter/basic.ts" startScript="generate,filter:basic" />
+
+## Relation filters
+
+Filters can be defined on conditions over relations. For one-to-one relations, you can filter on their fields directly. For one-to-many relations, use the "some", "every", or "none" operators to build a condition over a list of records.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="filter/relation.ts" startScript="generate,filter:relation" />
+
+## List filters
+
+List fields allow extra filter operators to filter on the list content:
+
+- `has`: checks if the list contains a specific value.
+- `hasEvery`: checks if the list contains all values in a given array.
+- `hasSome`: checks if the list contains at least one value in a given array.
+- `isEmpty`: checks if the list is empty.
+
+:::info
+List type is only supported for PostgreSQL.
+:::
+
+```zmodel
+model Post {
+  ...
+  topics String[]
+}
+```
+
+```ts
+await db.post.findMany({ 
+  where: { topics: { has: 'webdev' } } 
+});
+
+await db.post.findMany({
+  where: { topics: { hasSome: ['webdev', 'typescript'] } } 
+});
+
+await db.post.findMany({
+  where: { topics: { hasEvery: ['webdev', 'typescript'] } } 
+});
+
+await db.post.findMany({
+  where: { topics: { isEmpty: true } } 
+});
+```
+
+## Json filters
+
+:::info WORK IN PROGRESS
+Filtering on Json fields is work in progress and will be available soon.
+:::
+
+## Query builder filters
+
+<ZenStackVsPrisma>
+The ability to mix SQL query builder into ORM filters is a major improvement over Prisma.
+</ZenStackVsPrisma>
+
+ZenStack v3 is implemented on top of [Kysely](https://kysely.dev/), and it leverages Kysely's powerful query builder API to extend the filtering capabilities. You can use the `$expr` operator to define a boolean expression that can express almost everything that can be expressed in SQL.
+
+The `$expr` operator can be used together with other filter operators, so you can keep most of your filters simple and only reach to the query builder level for complicated components.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="filter/query-builder.ts" startScript="generate,filter:query-builder" />
diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md
new file mode 100644
index 00000000..738a5a93
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/find.md
@@ -0,0 +1,68 @@
+---
+sidebar_position: 2
+description: Find API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+import SelectIncludeOmit from './_select-include-omit.md';
+
+# Find
+
+The `find` series of APIs are used to query records from the database. It has the following methods:
+
+- `findMany`
+    
+    Find multiple records that match the query criteria.
+
+- `findUnique`
+    
+    Find a single record with a unique criteria.
+
+- `findFirst`
+    
+    Find the first record that matches the query criteria.
+
+- `findUniqueOrThrow`
+    
+    Similar to `findUnique`, but throws an error if no record is found.
+
+- `findFirstOrThrow`
+    
+    Similar to `findFirst`, but throws an error if no record is found.
+
+## Basic usage
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="find/basic.ts" startScript="generate,find:basic" />
+
+## Filtering
+
+The API provides a very flexible set of filtering options. We've put it into a [dedicated section](./filter.md).
+
+## Sorting
+
+Use the `sort` field to control the sort field, direction, and null field placement. Sorting is not supported for `findUnique` and `findUniqueOrThrow`.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="find/sort.ts" startScript="generate,find:sort" />
+
+## Pagination
+
+You can use two strategies for pagination: offset-based or cursor-based. Pagination is not supported for `findUnique` and `findUniqueOrThrow`.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="find/pagination.ts" startScript="generate,find:pagination" />
+
+## Field selection
+
+You can use the following fields to control what fields are returned in the result:
+
+<SelectIncludeOmit />
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="find/selection.ts" startScript="generate,find:selection" />
+
+## Finding distinct rows
+
+You can use the `distinct` field to find distinct rows based on specific fields. One row for each unique combination of the specified fields will be returned. The implementation relies on SQL `DISTINCT ON`, so it's not available for SQLite provider.
+
+```ts
+// returns one Post for each unique authorId
+await db.post.findMany({ distinct: ['authorId'] });
+```    
diff --git a/versioned_docs/version-3.x/orm/api/group-by.md b/versioned_docs/version-3.x/orm/api/group-by.md
new file mode 100644
index 00000000..92d7937e
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/group-by.md
@@ -0,0 +1,43 @@
+---
+sidebar_position: 8
+description: GroupBy API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# GroupBy
+
+The `groupBy` method allows you to group records by one or more fields and perform aggregations on the grouped records - very useful for generating summary statistics or reports.
+
+Use the `by` field to specify the field(s) to group by, and the following aggregation operators to perform on the grouped records:
+
+- `_count` - count the number of records in each group.
+- `_sum` - sum of a numeric field in each group.
+- `_avg` - average of a numeric field in each group.
+- `_min` - minimum value of a field in each group.
+- `_max` - maximum value of a field in each group.
+
+You can also use `where`, `orderBy`, `skip`, and `take` to control what records are included in the aggregation.
+
+The `having` field can be used to filter the aggregated results. Two types of filters can be used in the `having` clause:
+
+- A regular field that's used in the `by` clause, e.g.:
+
+    ```ts
+    await db.post.groupBy({ by: 'published', having: { published: true } });
+    ```
+
+- An aggregation, e.g.:
+
+    In this case, the fields of aggregation doesn't need to be in the `by` clause.
+
+    ```ts
+    await db.post.groupBy({
+      by: 'authorId',
+      having: { viewCount: { _sum: { gt: 100 }} }
+    });
+    ```
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-groupBy" openFile="main.ts" startScript="generate,dev" />
diff --git a/versioned_docs/version-3.x/orm/api/index.md b/versioned_docs/version-3.x/orm/api/index.md
new file mode 100644
index 00000000..6bd18a7d
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/index.md
@@ -0,0 +1,117 @@
+---
+sidebar_position: 4
+sidebar_label: Query API
+title: Query API
+---
+
+ZenStack ORM's query API provides a powerful and high-level way to interact with your database with awesome type safety. The API is a superset of [Prisma ORM's query API](https://www.prisma.io/docs/orm/prisma-client/queries), so if you are familiar with Prisma, you will feel right at home. If not, it's intuitive and easy to learn.
+
+The API is organized into several categories covered by the following sections. The API methods share many common input and output patterns, and we'll cover them in this overview section.
+
+## Common Input Fields
+
+- `where`
+
+    When an operation can involve filtering records, a `where` clause is used to specify the condition. E.g., `findUnique`, `updateMany`, `delete`, etc. `where` clause also exists in nested payload for filtering relations.
+
+    ```ts
+    await db.post.findMany({ where: { published: true } });
+    ```
+
+    The [Filter](./filter) section describes the filtering capabilities in detail.
+
+- `select`, `include`, `omit`
+
+    When an operation returns record(s), you can use these clauses to control the fields and relations returned in the result. The `select` clause is used to specify the fields/relations to return, `omit` to exclude, and `include` to include relations (together with all regular fields).
+
+    When selecting relations, you can nest these clauses to further control fields and relations returned in the nested relations.
+
+    ```ts
+    // results will include `title` field and `author` relation
+    await db.post.findMany({
+      select: { title: true, author: true },
+    });
+
+    // results will include all fields except `content`, plus `author` relation
+    await db.post.findMany({
+      omit: { content: true }, include: { author: true }
+    });
+    ```
+
+- `orderBy`, `take`, `skip`
+
+    When an operation returns multiple records, you can use these clauses to control the sort order, number of records returned, and the offset for pagination.
+
+    ```ts
+    // results will be sorted by `createdAt` in descending order, and return 
+    // 10 records starting from the 5th record
+    await db.post.findMany({ orderBy: { createdAt: 'desc' }, skip: 5, take: 10 });
+    ```
+
+- `data`
+
+    When an operation involves creating or updating records, a `data` clause is used to specify the data to be used. It can include nested objects for manipulating relations. See the [Create](./create) and [Update](./update) sections for details.
+
+    ```ts
+    // Create a new post and connect it to an author
+    await db.post.create({
+      data: { title: 'New Post', author: { connect: { id: 1 } } }
+    });
+    ```
+
+## Output Types
+
+The output types of the API methods generally fall into three categories:
+
+1. When the operation returns record(s)
+
+    The output type is "contextual" to the input's shape, meaning that when you specify `select`, `include`, or `omit` clauses, the output type will reflect that.
+
+    ```ts
+    // result will be `Promise<{ title: string; author: { name: string } }[]>`
+    await db.post.findMany({
+      select: { title: true, author: { select: { name: true } } }
+    });
+    ```
+
+2. When the operation returns a batch result
+
+    Some operations only returns a batch result `{ count: number }`, indicating the number of records affected. These include `createMany`, `updateMany`, and `deleteMany`.
+
+3. Aggregation
+
+    Aggregation operations' output type is contextual to the input's shape as well. See [Count](./count) and [Aggregate](./aggregate) sections for details.
+
+
+## Sample Schema
+
+Throughout the following sections, we will use the following ZModel schema as the basis for our examples:
+
+```zmodel title="zenstack/schema.zmodel"
+// This is a sample model to get you started.
+
+datasource db {
+    provider = 'sqlite'
+}
+
+/// User model
+model User {
+    id       Int    @id @default(autoincrement())
+    email    String @unique
+    posts    Post[]
+}
+
+/// Post model
+model Post {
+    id        Int      @id @default(autoincrement())
+    createdAt DateTime @default(now())
+    updatedAt DateTime @updatedAt
+    title     String
+    content   String?
+    slug      String?  @unique
+    published Boolean  @default(false)
+    viewCount Int      @default(0)
+    author    User?    @relation(fields: [authorId], references: [id])
+    authorId  Int?
+}
+```
diff --git a/versioned_docs/version-3.x/orm/api/transaction.md b/versioned_docs/version-3.x/orm/api/transaction.md
new file mode 100644
index 00000000..a0cf5767
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/transaction.md
@@ -0,0 +1,43 @@
+---
+sidebar_position: 9
+description: Transaction API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Transaction
+
+You can use the `$transaction` method to run multiple operations in a transaction. There are two overloads of this method:
+
+## Sequential Transaction
+
+This overload takes an array of promises as input. The operations are executed sequentially in the order they are provided. The operations are independent of each other, because there's no way to access the result of a previous operation and use it to influence the later operations.
+
+```ts
+// Note that the `db.user.create` and `db.post.create` calls are not awaited. They
+// are passed to the `$transaction` method to execute.
+const [user, post] = await db.$transaction([
+  db.user.create({ data: { name: 'Alice' } }),
+  db.user.create({ data: { name: 'Bob' } }),
+]);
+```
+
+The result of each operation is returned in the same order as the input.
+
+## Interactive Transaction
+
+This overload takes an async callback function as input. The callback receives a transaction client that can be used to perform database operations within the transaction.
+
+Interactive transactions allows you to write imperative code that can access the results of previous operations and use them to influence later operations. Albeit it's flexibility, you should make the transaction callback run as fast as possible so as to reduce the performance impact of the transaction on the database. 
+
+```ts
+const [user, post] = await db.$transaction(async (tx) => {
+  const user = await tx.user.create({ data: { name: 'Alice' } });
+  const post = await tx.post.create({ data: { title: 'Hello World', authorId: user.id } });
+  return [user, post];
+});
+```
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-transaction" openFile="main.ts" startScript="generate,dev" />
diff --git a/versioned_docs/version-3.x/orm/api/update.md b/versioned_docs/version-3.x/orm/api/update.md
new file mode 100644
index 00000000..9adcce77
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/api/update.md
@@ -0,0 +1,48 @@
+---
+sidebar_position: 4
+description: Update API
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Update
+
+Update to records can be done with the following methods:
+
+- `update` - Update a single, unique record.
+- `updateMany` - Update multiple records that match the query criteria.
+- `updateManyAndReturn` - Similar to `updateMany`, but returns the updated records
+- `upsert` - Update a single, unique record, or create it if it does not exist.
+
+## Updating scalar fields
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-update" openFile="scalar.ts" startScript="generate,scalar" />
+
+In additional to the standard way of updating fields, list fields support the following operators:
+
+- `push`: Append a value or a list of values to the end of the list.
+- `set`: Replace the entire list with a new list (equivalent to setting the field directly).
+
+```ts
+await db.post.update({
+  where: { id: '1' },
+  data: {
+    topics: { push: 'webdev'},
+  },
+});
+
+await db.post.update({
+  where: { id: '1' },
+  data: {
+    topics: { set: ['orm', 'typescript'] },
+  },
+});
+```
+
+## Manipulating relations
+
+THe `update` and `upsert` methods are very powerful in that they allow you to freely manipulate relations. You can create, connect, disconnect, update, and delete relations in a single operation. You can also reach deeply into indirect relations.
+
+`updateMany` and `updateManyAndReturn` only support updating scalar fields.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-update" openFile="relation.ts" startScript="generate,relation" />
diff --git a/versioned_docs/version-3.x/orm/cli.md b/versioned_docs/version-3.x/orm/cli.md
new file mode 100644
index 00000000..c798b998
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/cli.md
@@ -0,0 +1,18 @@
+---
+sidebar_position: 2
+description: Using the CLI
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Using the CLI
+
+ZenStack CLI is a command-line tool that takes the ZModel schema as input and complete different tasks for you. It's included in the "@zenstackhq/cli" package, and can be invoked with either `zen` or `zenstack` command (they are equivalent).
+
+In the context of ORM, the CLI compiles ZModel into a TypeScript representation, which can in turn be used to create a type-safe ORM client.
+
+You can try running the `npx zen generate` command in the following playground and inspect the TypeScript code generated inside the "zenstack" folder.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-quick-start" openFile="zenstack/schema.zmodel" />
+
+The `generate` command generates several TypeScript files from the ZModel schema that support both development-time typing and runtime access to the schema. For more details of the generated code, please refer to the [@core/typescript plugin](../reference/plugins/typescript.md) documentation.
diff --git a/versioned_docs/version-3.x/orm/client.md b/versioned_docs/version-3.x/orm/client.md
new file mode 100644
index 00000000..f3187fb4
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/client.md
@@ -0,0 +1,70 @@
+---
+sidebar_position: 3
+description: Creating a database client
+---
+
+import TabItem from '@theme/TabItem';
+import Tabs from '@theme/Tabs';
+import PackageInstall from '../_components/PackageInstall';
+import ZenStackVsPrisma from '../_components/ZenStackVsPrisma';
+
+# Database Client
+
+<ZenStackVsPrisma>
+Unlike Prisma, ZenStack doesn't bundle any database driver. You're responsible for installing a compatible one. Also it doesn't read database connection string from the schema. Instead, you pass in the connection information when creating the client.
+</ZenStackVsPrisma>
+
+The `zen generate` command compiles the ZModel schema into TypeScript code, which we can in turn use to initialize a type-safe database client. ZenStack uses Kysely to handle the low-level database operations, so the client is initialize with a [Kysely dialect](https://kysely.dev/docs/dialects) - an object that encapsulates database details.
+
+The samples below only show creating a client using SQLite (via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)) and PostgreSQL (via [node-postgres](https://github.com/brianc/node-postgres)), however you can also use any other Kysely dialects for these two types of databases.
+
+<Tabs>
+
+<TabItem value="sqlite" label={`SQLite`}>
+
+<PackageInstall dependencies={["better-sqlite3"]} devDependencies={['@types/better-sqlite3']} />
+
+```ts title='db.ts'
+import { ZenStackClient } from '@zenstackhq/runtime';
+import { SqliteDialect } from 'kysely';
+import SQLite from 'better-sqlite3';
+import { schema } from '@/zenstack/schema';
+
+export const db = new ZenStackClient(schema, {
+    dialect: new SqliteDialect({
+        database: new SQLite(':memory:'),
+    }),
+});
+```
+</TabItem>
+
+<TabItem value="postgres" label={`PostgreSQL`}>
+
+<PackageInstall dependencies={["pg"]} devDependencies={['@types/pg']} />
+
+```ts title='db.ts'
+import { ZenStackClient } from '@zenstackhq/runtime';
+import { schema } from '@/zenstack/schema';
+import { PostgresDialect } from 'kysely';
+import { Pool } from 'pg';
+
+export const db = new ZenStackClient(schema, {
+    dialect: new PostgresDialect({
+        pool: new Pool({
+            connectionString: process.env.DATABASE_URL,
+        }),
+    }),
+});
+```
+</TabItem>
+
+</Tabs>
+
+The created `db` object has the full ORM API inferred from the type of the `schema` parameter. When necessary, you can also explicitly get the inferred client type like:
+
+```ts
+import type { ClientContract } from '@zenstackhq/runtime';
+import type { SchemaType } from '@/zenstack/schema';
+
+export type DbClient = ClientContract<SchemaType>;
+```
diff --git a/versioned_docs/version-3.x/orm/computed-fields.md b/versioned_docs/version-3.x/orm/computed-fields.md
new file mode 100644
index 00000000..889cca12
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/computed-fields.md
@@ -0,0 +1,50 @@
+---
+sidebar_position: 10
+description: Computed fields in ZModel
+---
+
+import ZenStackVsPrisma from '../_components/ZenStackVsPrisma';
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Computed Fields
+
+<ZenStackVsPrisma>
+Prisma client extensions allow you to define computed fields. ZenStack's approach is very different in two aspects:
+1. Computed fields are evaluated on the database side, not in the client.
+2. Computed fields are defined in the schema and can be used in most places where regular fields are used.
+</ZenStackVsPrisma>
+
+Computed fields are "virtual" fields that do not physically exist in the database. They are computed on the fly, but other than that, they behave like regular fields. They are returned as part of the query results, can be used for filtering, sorting, etc., and can be used to define access policies.
+
+## Defining Computed Fields
+
+Defining a computed fields involves two steps. First, add the field in the ZModel schema to a model and annotate it with an extra `@computed` attribute.
+
+```zmodel
+model User {
+    ...
+    postCount Int @computed
+}
+```
+
+Then, when creating a `ZenStackClient`, provide the implementation of the field using the Kysely query builder.
+
+```ts
+import { ZenStackClient } from '@zenstackhq/runtime';
+
+const db = new ZenStackClient(schema, {
+  ...
+  computedFields: {
+    User: {
+      postCount: (eb) => 
+        eb.selectFrom('Post')
+          .whereRef('Post.authorId', '=', 'User.id')
+          .select(({fn}) => fn.countAll<number>().as('count')),
+    },
+  },
+});
+```
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-computed-fields" />
diff --git a/versioned_docs/version-3.x/orm/index.md b/versioned_docs/version-3.x/orm/index.md
new file mode 100644
index 00000000..3cd0ddd5
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/index.md
@@ -0,0 +1,117 @@
+---
+sidebar_position: 1
+description: ZenStack ORM overview
+---
+
+import ZenStackVsPrisma from '../_components/ZenStackVsPrisma';
+
+# ORM Overview
+
+ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and strives to provide an awesome developer experience by combining the best ingredients into a cohesive package.
+
+## Key Features
+
+### [Prisma](https://prisma.io/orm)-compatible query API
+
+ZenStack v3 is inspired by Prisma ORM but it has a completely different implementation (based on [Kysely](https://kysely.dev/)). On the surface, it replicated Prisma ORM's query API so that you can use it pretty much as a drop-in replacement. Even if you're not a Prisma user, the query API is very intuitive and easy to learn.
+
+```ts
+await db.user.findMany({
+    where: {
+        email: {
+            endsWith: 'zenstack.dev',
+        },
+    },
+    orderBy: {
+        createdAt: 'desc',
+    },
+    include: { posts: true }
+});
+```
+
+### Low-level query builder powered by [Kysely](https://kysely.dev/)
+
+ORM APIs are concise and pleasant, but they have their limitations. When you need extra power, you can fall back to the low-level query builder API powered by [Kysely](https://kysely.dev/) - limitless expressiveness, full type safety, and zero extra setup.
+
+```ts
+await db.$qb
+    .selectFrom('User')
+    .leftJoin('Post', 'Post.authorId', 'User.id')
+    .select(['User.id', 'User.email', 'Post.title'])
+    .execute();
+```
+
+### Access control
+
+ZenStack ORM comes with a powerful built-in access control system. You can define access rules right inside the schema. The rules are enforced at runtime via query injection, so it doesn't rely on any database specific row-level security features.
+
+```zmodel"
+model Post {
+    id        Int     @id
+    title     String  @length(1, 256)
+    published Boolean @default(false)
+    author    User    @relation(fields: [authorId], references: [id])
+    authorId  Int
+
+    // no anonymous access
+    @@deny('all', auth() == null)
+
+    // author has full access
+    @@allow('all', authorId == auth().id)
+
+    // published posts are readable by anyone
+    @@allow('read', published)
+}
+```
+
+### Polymorphic models
+
+Real-world applications often involves storing polymorphic data which is notoriously complex to model and query. ZenStack does the heavy-lifting for you so you can model an inheritance hierarchy with simple annotations, and query them with perfect type safety.
+
+```zmodel title="zenstack/schema.zmodel"
+model Content {
+    id    Int    @id
+    name  String @length(1, 256)
+    type  String
+
+    // the ORM uses the `type` field to determine to which concrete model
+    // a query should be delegated
+    @@delegate(type)
+}
+
+model Post extends Content {
+    content String
+}
+```
+
+```ts title="main.ts"
+const content = await db.content.findFirstOrThrow();
+if (content.type === 'Post') {
+    // content's type is narrowed down to `Post`
+    console.log(content.content);
+} else {
+    // other asset type
+}
+```
+
+### Straightforward and light-Weighted
+
+Compared to Prisma and previous versions of ZenStack, v3 is more straightforward and light-weighted.
+
+- No runtime dependency to Prisma, thus no overhead of Rust/WASM query engines.
+- No magic generating into `node_modules`. You fully control how the generated code is compiled and bundled.
+- Less code generation, more type inference.
+
+## Documentation Conventions
+
+### Sample playground
+
+Throughout the documentation we'll use [StackBlitz](https://stackblitz.com/) to provide interactive code samples. StackBlitz's [WebContainers](https://webcontainers.io/) is an awesome technology that allows you to run a Node.js environment inside the browser. The embedded samples use the [sql.js](https://github.com/sql-js/sql.js) (a WASM implementation of SQLite) for WebContainers compatibility, which is not suitable for production use.
+
+### If you already know Prisma
+
+Although ZenStack ORM has a Prisma-compatible query API, the documentation doesn't assume prior knowledge of using Prisma. However, readers already familiar with Prisma can quickly skim through most of the content and focus on the differences. The documentation uses the following callout to indicate major differences between ZenStack ORM and Prisma:
+
+<ZenStackVsPrisma>
+Explanation of some key differences between ZenStack and Prisma ORM.
+</ZenStackVsPrisma>
diff --git a/versioned_docs/version-3.x/orm/inferred-types.md b/versioned_docs/version-3.x/orm/inferred-types.md
new file mode 100644
index 00000000..4ac02b32
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/inferred-types.md
@@ -0,0 +1,26 @@
+---
+sidebar_position: 14
+description: TypeScript types derived from the ZModel schema
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Schema-Inferred Types
+
+Most of the time, you don't need to explicitly type the input and output of the ORM methods, thanks to TypeScript's powerful inference capabilities. However, when you do have the need, you can rely on the following utilities to type things:
+
+- `zenstack/models`
+
+    The `zen generate` command generates a `models` module that exports types for all models, types, and enums. The model types include all scalar fields (including computed ones).
+
+- `zenstack/input`
+
+    The `zen generate` command generates an `input` module that exports types for input arguments of the ORM methods, such as `UserCreateArgs`, `PostUpsertArgs`, etc. You can use them to type intermediary variables that are later passed to the ORM methods.
+
+- `ModelResult`
+
+    The `ModelResult` generic type from `@zenstackhq/runtime` allows you to infer the exact model type given field selection and relation inclusion information.
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="inferred-types.ts" startScript="generate" />
diff --git a/versioned_docs/version-3.x/orm/logging.md b/versioned_docs/version-3.x/orm/logging.md
new file mode 100644
index 00000000..a6a4ad1d
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/logging.md
@@ -0,0 +1,44 @@
+---
+sidebar_position: 13
+description: Setup logging
+---
+
+# Logging
+
+Logging can be enabled by passing a `log` option when creating a `ZenStackClient` instance. The log option can be a list of log levels, causing the client to log messages at those levels to the console:
+
+```ts
+const db = new ZenStackClient(..., {
+  log: ['query', 'error'],
+});
+```
+
+Or it can be a logging function that receives a log object with the following type:
+
+```ts
+// https://kysely.dev/docs/recipes/logging
+
+interface LogEvent {
+  // log level
+  level: 'query' | 'error';
+
+  // the query compiled by Kysely, including the SQL AST, raw SQL string, and parameters.
+  query: CompiledQuery;
+  
+  // time taken to execute the query
+  queryDurationMillis: number; 
+
+  // error information, only present if `level` is `'error'`
+  error: unknown;
+}
+```
+
+```ts
+const db = new ZenStackClient(..., {
+  log: (event) => {
+    console.log(`[${event.level}] ${event.queryDurationMillis}ms`);
+  },
+});
+```
+
+The `log` option is passed through to the underlying Kysely instance.
diff --git a/versioned_docs/version-3.x/orm/migration.md b/versioned_docs/version-3.x/orm/migration.md
new file mode 100644
index 00000000..67dc6cad
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/migration.md
@@ -0,0 +1,62 @@
+---
+sidebar_position: 15
+description: Schema migration introduction
+---
+
+# Database Schema Migration
+
+Database schema migration is a crucial aspect of application development. It helps you keep your database schema in sync with your data model, and ensures deployments are smooth and predictable. ZenStack provides migration tools that create migration scripts based on the ZModel schema and apply them to the database.
+
+## Introduction
+
+ZenStack migration is built on top of [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate). The `zen` CLI provides a set of migration-related commands that are simple wrappers around Prisma CLI. When running these commands, the CLI automatically generates a Prisma schema from ZModel, and then executes the corresponding Prisma CLI.
+
+Basing on Prisma Migrate brings the following benefits:
+
+- **No reinventing the wheels**: Prisma Migrate is a mature and widely used migration tool, ensuring reliability and stability.
+
+- **Backward compatibility**: When migrating from Prisma to ZenStack, you can continue using your existing migration scripts without any changes.
+
+If you are already familiar with Prisma Migrate, you can continue using your current workflow with ZenStack, simply swapping `prisma` CLI with `zen`. If not, this section will guide you through the commands and common workflows, but it's strongly recommended to check [Prisma Migrate documentation](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/overview) for in-depth information too.
+
+## Commands
+
+Please refer to the [CLI Reference](../reference/cli.md) for the full list of commands and options.
+
+### db push
+
+The `zen db push` command is used to push your ZModel schema changes to the database without creating a migration file. This is useful for development and testing purposes. Never use it in production.
+
+### migrate dev
+
+The `zen migrate dev` command is used to create a new migration file based on your ZModel schema changes. It will also apply the migration to your database. The command is for development only and should never be used in production.
+
+### migrate deploy
+
+The `zen migrate deploy` command is used to apply all pending migrations to your production database. This is typically run in your deployment pipeline.
+
+### migrate reset
+
+The `zen migrate reset` command is used to reset your database to a clean state by dropping all tables and reapplying all migrations. This is useful for testing and development purposes.
+
+### migrate status
+
+The `zen migrate status` command is used to check the status of your migrations. It will show you which migrations have been applied and which are pending.
+
+### migrate resolve
+
+The `zen migrate resolve` command is used to mark a migration as applied or rolled back without changing the database schema. This is useful for situations where you need to manually intervene in the migration process.
+
+## Workflow
+
+### Typical development workflow
+
+1. Make changes to your ZModel schema.
+2. Run `zen db push` to push the changes to your database without creating a migration file.
+3. Test the changes locally.
+4. Run `zen migrate dev` to create a migration file. You'll be promoted if a full reset is needed.
+5. Carefully review the migration file, make changes as needed, and commit it to source control.
+
+### Typical deployment workflow
+
+The most common practice is to run `zen migrate deploy` as part of your production deployment pipeline, which simply applies all pending migrations to the database.
diff --git a/versioned_docs/version-3.x/orm/plugins/_category_.yml b/versioned_docs/version-3.x/orm/plugins/_category_.yml
new file mode 100644
index 00000000..12d5462a
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/plugins/_category_.yml
@@ -0,0 +1,4 @@
+position: 12
+label: Plugins
+collapsible: true
+collapsed: true
diff --git a/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md b/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md
new file mode 100644
index 00000000..d231060f
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 2
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Entity Mutation Hooks
+
+## Introduction
+
+Entity mutation hooks allow you to intercept entity mutation operations, i.e., "create", "update", and "delete". They are triggered regardless of whether the operations are performed through the ORM queries or the query builder API.
+
+To create an entity mutation hook plugin, call the `$use` method with an `onEntityMutation` key containing an object with the following fields (all optional):
+
+- `beforeEntityMutation`
+    A callback function that is called before the entity mutation operation. It receives a context object containing:
+      - The model
+      - The action (create, update, delete)
+      - The Kysely query node (SQL AST)
+      - The entities being mutated (only available when opted in)
+
+- `afterEntityMutation`
+    A callback function that is called after the entity mutation operation. It receives a context object containing:
+      - The model
+      - The action (create, update, delete)
+      - The Kysely query node (SQL AST)
+      - The entities before the mutation (only available when opted in)
+      - The entities after the mutation (only available when opted in)
+
+- `mutationInterceptionFilter`
+  
+    A callback used to determine if an operation should be intercepted, and if so, whether entities should be loaded and passed to the `beforeEntityMutation` and `afterEntityMutation` hooks. It receives a context object containing:
+      - The model
+      - The action (create, update, delete)
+      - The Kysely query node (SQL AST)
+    
+    The callback should return an object indicating if the operation should be intercepted and whether entities should be loaded. If this callback is not provided, by default, all mutation operations are intercepted, but entities are not loaded.
+
+If a mutation happens inside a transaction, the `afterEntityMutation` callback is called after the transaction is committed.
+
+:::info
+Update and delete triggered by cascading operations are not captured by the entity mutation hooks.
+:::
+
+:::warning
+Be very careful about opting in to load before and after mutation entities. Batch mutations can result in a large number of entities being loaded and incur significant performance overhead.
+:::
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="plugins/entity-mutation-hooks.ts" startScript="generate,entity-mutation-hooks" />
diff --git a/versioned_docs/version-3.x/orm/plugins/index.md b/versioned_docs/version-3.x/orm/plugins/index.md
new file mode 100644
index 00000000..e10da89d
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/plugins/index.md
@@ -0,0 +1,42 @@
+---
+sidebar_position: 1
+description: ORM plugin introduction
+---
+
+import ZenStackVsPrisma from '../../_components/ZenStackVsPrisma';
+
+# Plugin Overview
+
+<ZenStackVsPrisma>
+ZenStack's plugin system aims to provide a more flexible extensibility solution than [Prisma Client Extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions), allowing you to tap into the ORM runtime at different levels. Some parts of the plugin design resemble client extensions, but overall it's not meant to be compatible.
+</ZenStackVsPrisma>
+
+As you go deeper using an ORM, you'll find the need to tap into its engine for different purposes. For example, you may want to:
+
+- Log the time cost of each query operation.
+- Block certain CRUD operations.
+- Execute code when an entity is created, updated, or deleted.
+- Alter the SQL query before it's sent to the database.
+- ...
+
+ZenStack ORM provides three ways for you to tap into its runtime:
+
+1. **Query API hooks**
+   
+    Query API hooks allow you to intercept ORM query operations (`create`, `findUnique`, etc.). You can execute arbitrary code before or after the query operation, or even block the operation altogether. See [Query API Hooks](./query-api-hooks.md) for details.
+
+2. **Entity mutation hooks**
+   
+    Entity mutation hooks allow you to execute code before or after an entity is created, updated, or deleted. This is very useful when you only care about entity changes instead of what triggered the changes. See [Entity Mutation Hooks](./entity-mutation-hooks.md) for details.
+
+3. **Kysely query hooks**
+
+    Kysely query hooks give you the ultimate power to inspect and alter the SQL query (in AST form) before it's sent to the database. This is a very powerful low-level extensibility that should be used with care. See [Kysely Query Hooks](./kysely-query-hooks.md) for details.
+
+All three types of plugins are installed via the unified `$use` method on the ORM client. The `$use` method returns a new ORM client with the plugin applied, without modifying the original client. You can use the `$unuse` or `$unuseAll` methods to remove plugin(s) from a client.
+
+```ts
+const db = new ZenStackClient({ ... });
+const withPlugin = $db.use({ ... });
+const noPlugin = withPlugin.$unuseAll();
+```
diff --git a/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md b/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md
new file mode 100644
index 00000000..4d7bca47
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md
@@ -0,0 +1,28 @@
+---
+sidebar_position: 3
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Kysely Query Hooks
+
+## Introduction
+
+Kysely query hooks are the lowest level of interceptor in the plugin system. Since ZenStack eventually delegates all database access to Kysely, these hooks allow you to inspect and alter all SQL queries before they are sent to the database, regardless of whether they originate from the ORM query API or the query builder API.
+
+This mechanism gives you great power to entirely control the ORM's behavior. One good example is the [access policy](../access-control/) - the access policy enforcement is entirely achieved via intercepting the Kysely queries.
+
+To create a Kysely query hook plugin, call the `$use` method with an object containing a `onKyselyQuery` callback. The callback is triggered before each Kysely query is executed. It receives a context object containing:
+
+- The Kysely instance
+- The Kysely query node (SQL AST)
+- The ORM client that triggered the query
+- A "proceed query" function, which you can call to send the query to the database
+
+## Samples
+
+:::info
+Kysely's `QueryNode` objects are low-level and not easy to process. ZenStack will provide helpers to facilitate common tasks in the future.
+:::
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="plugins/kysely-query-hooks.ts" startScript="generate,kysely-query-hooks" />
diff --git a/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md b/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md
new file mode 100644
index 00000000..2ee43085
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md
@@ -0,0 +1,25 @@
+---
+sidebar_position: 1
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Query API Hooks
+
+## Introduction
+
+Query API hooks allow you to intercept ORM queries, like `create`, `findUnique`, etc. You can execute arbitrary code before or after the query operation, modify query args, or even block the operation altogether.
+
+To create a query API hook plugin, call the `$use` method with an object with the `onQuery` key providing a callback. The callback is invoked with an argument containing the following fields:
+
+- The model
+- The operation
+- The query args
+- The ORM client that triggered the query
+- A "proceed query" function, which you can call to continue executing the operation
+
+As its name suggests, query API hooks are only triggered by ORM query calls, not by query builder API calls.
+
+## Samples
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="plugins/query-api-hooks.ts" startScript="generate,query-api-hooks" />
diff --git a/versioned_docs/version-3.x/orm/polymorphism.md b/versioned_docs/version-3.x/orm/polymorphism.md
new file mode 100644
index 00000000..0b47630c
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/polymorphism.md
@@ -0,0 +1,36 @@
+---
+sidebar_position: 11
+description: Polymorphic models
+---
+
+import ZenStackVsPrisma from '../_components/ZenStackVsPrisma';
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+import GithubCodeBlock from '@site/src/components/GithubCodeBlock';
+
+# Polymorphic Models
+
+<ZenStackVsPrisma>
+Polymorphic models is a major feature that sets ZenStack apart from Prisma.
+</ZenStackVsPrisma>
+
+ZenStack natively supports polymorphic models. As we have seen in the [Polymorphism](../modeling/polymorphism.md) section in the data modeling part, the ZModel language allows you to define models with Object-Oriented style inheritance. This section will describe the ORM runtime behavior of polymorphic models.
+
+## CRUD behavior
+
+Polymorphic models' CRUD behavior is similar to that of regular models, with two major differences:
+
+1. Base model entities cannot be created directly as they cannot exist without an associated concrete model entity.
+2. When querying a base model (either top-level or nested), the result will include all fields of the associated concrete model (unless fields are explicitly selected). The result's type is a discriminated union, so you can use TypeScript's type narrowing to access the concrete model's specific fields.
+
+The ORM query API hides all the complexity of managing polymorphic models for you:
+- When creating a concrete model entity, its base entity is automatically created.
+- When querying a base entity, the ORM fetches the associated concrete entity and merges the results.
+- When deleting a base or concrete entity, the ORM automatically deletes the counterpart entity.
+
+## Samples
+
+The schema used in the sample involves a base model and three concrete models:
+
+<GithubCodeBlock repoPath="zenstackhq/v3-doc-orm-polymorphism" file="zenstack/schema.zmodel"/>
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm-polymorphism" openFile="main.ts" startScript="generate,dev" />
diff --git a/versioned_docs/version-3.x/orm/query-builder.md b/versioned_docs/version-3.x/orm/query-builder.md
new file mode 100644
index 00000000..af26c2e9
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/query-builder.md
@@ -0,0 +1,27 @@
+---
+sidebar_position: 5
+description: Query builder API
+---
+
+import ZenStackVsPrisma from '../_components/ZenStackVsPrisma';
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+
+# Query Builder API
+
+<ZenStackVsPrisma>
+Query builder API is a major feature that sets ZenStack apart from Prisma.
+</ZenStackVsPrisma>
+
+The [Query API](./api/) introduced in the previous sections provide a powerful and intuitive way to query databases. However, complex applications usually have use cases that outgrow its capabilities. For typical ORMs, this is where you leave the comfort zone and resort to writing SQL.
+
+The unique advantage of ZenStack is that it's built above [Kysely](https://kysely.dev) - a highly popular, well-designed and type-safe SQL query builder. This means we can easily expose the full power of Kysely to you as a much better alternative to writing raw SQL.
+
+No extra setup is needed to use the query builder API. The ORM client has a `$qb` property that provides the Kysely query builder, and it's typing is inferred from the ZModel schema.
+
+Besides building full queries, the query builder API can also be embedded inside the ORM query API with a `$expr` key inside a `where` clause. See [Filter](./api/filter.md) section for details.
+
+## Samples
+
+The samples assume you have a basic understanding of Kysely.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-orm" openFile="query-builder.ts" startScript="generate,query-builder" />
diff --git a/versioned_docs/version-3.x/orm/quick-start.md b/versioned_docs/version-3.x/orm/quick-start.md
new file mode 100644
index 00000000..1cc2ad48
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/quick-start.md
@@ -0,0 +1,69 @@
+---
+sidebar_position: 1
+description: Quick start guide
+---
+
+import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed';
+import ZModelStarter from '../_components/_zmodel-starter.md';
+import PackageInstall from '../_components/PackageInstall.tsx';
+import PackageExec from '../_components/PackageExec.tsx';
+
+# Quick Start
+
+:::info
+All v3 packages are currently published under the "@next" tag.
+:::
+
+There are several ways to start using ZenStack ORM.
+
+## 1. Creating a project from scratch
+
+Run the following command to scaffold a new project with a pre-configured minimal starter:
+
+```bash
+npm create zenstack@next my-project
+```
+
+Or simply use the following playground to experience it inside the browser.
+
+<StackBlitzGithubEmbed repoPath="zenstackhq/v3-doc-quick-start" />
+
+## 2. Adding to an existing project
+
+To add ZenStack to an existing project, run the CLI `init` command to install dependencies and create a sample schema:
+
+<PackageExec command="zenstack@next init" />
+
+Then create a `zenstack/schema.zmodel` file in the root of your project. You can use the following sample schema to get started:
+
+<ZModelStarter />
+
+Finally, run `zen generate` to compile the schema into TypeScript.
+
+<PackageExec command="zen generate" />
+
+## 3. Manual setup
+
+You can also always configure a project manually with the following steps:
+
+1. Install dependencies
+
+  <PackageInstall devDependencies={['@zenstackhq/cli@next']} dependencies={['@zenstackhq/runtime@next']} />
+
+2. Create a `zenstack/schema.zmodel` file
+
+  You can use the following sample schema to get started:
+
+  <ZModelStarter />
+
+3. Run the CLI `generate` command to compile the schema into TypeScript
+
+  <PackageExec command="zen generate" />
+
+:::info
+
+By default, ZenStack CLI loads the schema from `zenstack/schema.zmodel`. You can change this by passing the `--schema` option. TypeScript files are by default generated to the same directory as the schema file. You can change this by passing the `--output` option.
+
+You can choose to either commit the generated TypeScript files to your source control, or add them to `.gitignore` and generate them on the fly in your CI/CD pipeline.
+
+:::
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/orm/validation.md b/versioned_docs/version-3.x/orm/validation.md
new file mode 100644
index 00000000..d505d5a9
--- /dev/null
+++ b/versioned_docs/version-3.x/orm/validation.md
@@ -0,0 +1,8 @@
+---
+sidebar_position: 9
+description: Data validation in ZModel
+---
+
+# Data Validation 🚧
+
+Coming soon 🚧
diff --git a/versioned_docs/version-3.x/recipes/_category_.yml b/versioned_docs/version-3.x/recipes/_category_.yml
new file mode 100644
index 00000000..acb7bc3f
--- /dev/null
+++ b/versioned_docs/version-3.x/recipes/_category_.yml
@@ -0,0 +1,7 @@
+position: 5
+label: Recipes
+collapsible: true
+collapsed: true
+link:
+    type: generated-index
+    title: Recipes
diff --git a/versioned_docs/version-3.x/reference/_category_.yml b/versioned_docs/version-3.x/reference/_category_.yml
new file mode 100644
index 00000000..1352c105
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/_category_.yml
@@ -0,0 +1,7 @@
+position: 7
+label: Reference
+collapsible: true
+collapsed: true
+link:
+    type: generated-index
+    title: Reference
diff --git a/versioned_docs/version-3.x/reference/api.md b/versioned_docs/version-3.x/reference/api.md
new file mode 100644
index 00000000..52b981ba
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/api.md
@@ -0,0 +1,7 @@
+---
+description: API references
+sidebar_position: 4
+sidebar_label: API
+---
+
+# API Reference
diff --git a/versioned_docs/version-3.x/reference/cli.md b/versioned_docs/version-3.x/reference/cli.md
new file mode 100644
index 00000000..e6188f8b
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/cli.md
@@ -0,0 +1,251 @@
+---
+description: CLI references
+sidebar_position: 2
+sidebar_label: CLI
+---
+
+# ZenStack CLI Reference
+
+## Usage
+
+```
+zenstack [options] [command]
+
+ζ ZenStack is a Prisma power pack for building full-stack apps.
+
+Documentation: https://zenstack.dev.
+
+Options:
+  -v --version           display CLI version
+  -h, --help             display help for command
+
+Commands:
+  info [path]            Get information of installed ZenStack and related packages.
+  init [options] [path]  Initialize an existing project for ZenStack.
+  generate [options]     Generates RESTful API and Typescript client for your data model.
+  repl [options]         Start a REPL session.
+  format [options]       Format a ZenStack schema file.
+  check [options]        Check a ZenStack schema file for syntax or semantic errors.
+  help [command]         Display help for a command.
+```
+
+## Sub Commands
+
+### info
+
+Get information of installed ZenStack and related packages.
+
+```bash
+zenstack info [options] [path]
+```
+
+#### Arguments
+
+| Name | Description  | Default        |
+| ---- | ------------ | -------------- |
+| path | Project path | current folder |
+
+### init
+
+Initializes an existing project to use ZenStack.
+
+```bash
+zenstack init [options] [path]
+```
+
+#### Arguments
+
+| Name | Description  | Default        |
+| ---- | ------------ | -------------- |
+| path | Project path | current folder |
+
+#### Options
+
+| Name                  | Description                                      | Default                                   |
+| --------------------- | ------------------------------------------------ | ----------------------------------------- |
+| --prisma              | location of Prisma schema file to bootstrap from | &lt;project path&gt;/prisma/schema.prisma |
+| -p, --package-manager | package manager to use: "npm", "yarn", or "pnpm" | auto detect                               |
+| --no-version-check    | do not check for new versions of ZenStack        | false                  |
+
+#### Examples
+
+Initialize current folder with default settings.
+
+```bash
+npx zenstack init
+```
+
+Initialize "myapp" folder with custom package manager and schema location.
+
+```bash
+npx zenstack init -p pnpm --prisma prisma/my.schema myapp
+```
+
+### generate
+
+Generates Prisma schema and other artifacts as specified by "plugin"s in ZModel.
+
+```bash
+zenstack generate [options]
+```
+
+#### Arguments
+
+| Name | Description  | Default        |
+| ---- | ------------ | -------------- |
+| path | Project path | current folder |
+
+#### Options
+
+| Name                  | Description                                      | Default                |
+| --------------------- | ------------------------------------------------ | ---------------------- |
+| --schema              | schema file (with extension .zmodel)             | ./schema.zmodel        |
+| -o, --output &lt;path&gt;   | default output directory for TS/JS files generated by built-in plugins   | node_modules/.zenstack |
+| --with-plugins        | only run specific plugins                        |                        |
+| --without-plugins     | exclude specific plugins                         |                        |
+| --no-default-plugins  | do not automatically run built-in plugins        | false                  |
+| --no-compile          | do not compile the output of built-in plugins    | false                  |
+| --no-version-check    | do not check for new versions of ZenStack        | false                  |
+
+You can also specify the ZModel schema location in the "package.json" file of your project like the following:
+
+```json title="package.json"
+{
+  "zenstack": {
+    "schema": "./db/schema.zmodel"
+  }
+}
+```
+
+#### Examples
+
+Generate with default settings.
+
+```bash
+npx zenstack generate
+```
+
+Generate with custom schema location.
+
+```bash
+npx zenstack generate --schema src/my.zmodel
+```
+
+### repl
+
+Starts a REPL session. You should run the command inside the package where you ran `zenstack generate`.
+
+```bash
+npx zenstack repl
+```
+
+You can call PrismaClient methods interactively in the REPL session. The following variables are available in the REPL session.
+
+- `prisma`
+
+    The original PrismaClient instance (without ZenStack enhancement).
+
+- `db`
+
+    The ZenStack enhanced PrismaClient instance.
+
+You don't need to `await` the Prisma method call result. The REPL session will automatically await and print the result.
+
+#### Options
+
+| Name  | Description         | Default |
+| ----- | ------------------- | ------- |
+| --debug | Enable debug output. Can be toggled on the fly in the repl session with the ".debug" command. | false |
+| --table | Enable table format. Can be toggled on the fly in the repl session with the ".table" command. | false |
+| --prisma-client &lt;path&gt; | Path to load PrismaClient module. | "node_modules/.prisma/client" |
+| --load-path &lt;path&gt; | Path to load modules generated by ZenStack. | "node_modules/.zenstack" |
+
+#### Repl Commands
+
+You can use the following commands in the REPL session.
+
+- `.debug [on|off]`
+  
+    Toggle debug output.
+
+- `.table [on|off]`
+
+    Toggle table format.
+
+- `.auth [user object]`
+
+    Set current user. E.g.: `.auth { id: 1 }`. Run the command without argument to reset to anonymous user.
+
+#### Examples
+
+Start the session:
+```bash
+npx zenstack repl
+```
+
+Inside the session:
+```ts
+> prisma.user.findMany()
+[
+  {
+    id: '7aa301d2-7a29-4e1e-a041-822913a3ea78',
+    createdAt: 2023-09-05T04:04:43.793Z,
+    updatedAt: 2023-09-05T04:04:43.793Z,
+    email: 'yiming@whimslab.io',
+    ...
+  }
+]
+
+> .auth { id: '7aa301d2-7a29-4e1e-a041-822913a3ea78' }
+Auth user: { id: '7aa301d2-7a29-4e1e-a041-822913a3ea78' }. Use ".auth" to switch to anonymous.
+
+> .table
+Table output: true
+
+> db.list.findMany({select: { title: true, private: true}})
+┌─────────┬────────────────────────────────────────────────────┬─────────┐
+│ (index) │                       title                        │ private │
+├─────────┼────────────────────────────────────────────────────┼─────────┤
+│    0    │                        'L1'                        │  false  │
+│    1    │               'Wonderful new world'                │  false  │
+│    2    │ 'Model for a space in which users can collaborate' │  false  │
+└─────────┴────────────────────────────────────────────────────┴─────────┘
+```
+
+### format
+
+Format a ZenStack schema file.
+
+```bash
+zenstack format [options]
+```
+
+#### Options
+
+| Name                  | Description                                      | Default                |
+| --------------------- | ------------------------------------------------ | ---------------------- |
+| --schema              | schema file (with extension .zmodel)             | ./schema.zmodel        |
+
+You can also specify the ZModel schema location in the "package.json" file of your project like the following:
+
+```json title="package.json"
+{
+  "zenstack": {
+    "schema": "./db/schema.zmodel"
+  }
+}
+```
+
+### check
+
+Check a ZenStack schema file for syntax or semantic errors. You can use the CLI's exit code to determine if the schema is valid.
+
+```bash
+zenstack check [options]
+```
+
+#### Options
+
+| Name                  | Description                                      | Default                |
+| --------------------- | ------------------------------------------------ | ---------------------- |
+| --schema              | schema file (with extension .zmodel)             | ./schema.zmodel        |
diff --git a/versioned_docs/version-3.x/reference/plugin-dev.md b/versioned_docs/version-3.x/reference/plugin-dev.md
new file mode 100644
index 00000000..658cf50b
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/plugin-dev.md
@@ -0,0 +1,8 @@
+---
+sidebar_position: 5
+description: Plugin development guide
+---
+
+# Plugin Development 🚧
+
+Coming soon 🚧
diff --git a/versioned_docs/version-3.x/reference/plugins/_category_.yml b/versioned_docs/version-3.x/reference/plugins/_category_.yml
new file mode 100644
index 00000000..d74396e4
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/plugins/_category_.yml
@@ -0,0 +1,7 @@
+position: 4
+label: Plugins
+collapsible: true
+collapsed: true
+link:
+    type: generated-index
+    title: Plugin Reference
diff --git a/versioned_docs/version-3.x/reference/plugins/prisma.md b/versioned_docs/version-3.x/reference/plugins/prisma.md
new file mode 100644
index 00000000..2e40f1b6
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/plugins/prisma.md
@@ -0,0 +1,24 @@
+---
+sidebar_position: 2
+sidebar_label: "@core/prisma"
+description: Generating Prisma schema from ZModel
+---
+
+# @core/prisma
+
+The `@core/prisma` plugin generates a Prisma schema file from ZModel.
+
+Please note that ZenStack's ORM runtime doesn't depend on Prisma, so you don't need to use this plugin to use the ORM. However, you can use it to generate a Prisma schema and then run custom Prisma generators or other tools that consumes a Prisma schema.
+
+## Options
+
+- `output`: Specifies the path of the generated Prisma schema file. If a relative path is provided, it will be resolved relative to the ZModel schema.
+
+## Example
+
+```zmodel
+plugin prisma {
+  provider = '@core/prisma'
+  output = '../prisma/schema.prisma'
+}
+```
diff --git a/versioned_docs/version-3.x/reference/plugins/typescript.md b/versioned_docs/version-3.x/reference/plugins/typescript.md
new file mode 100644
index 00000000..d14d5a98
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/plugins/typescript.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 1
+sidebar_label: "@core/typescript"
+description: Generating TypeScript code from ZModel
+---
+
+# @core/typescript
+
+The `@core/typescript` plugin generates TypeScript code from ZModel. The generated code is used to access the schema at runtime, as well as type declarations at development time.
+
+## Options
+
+- `output`: Specifies the output directory for the generated TypeScript code. If a relative path is provided, it will be resolved relative to the ZModel schema.
+
+## Output
+
+The plugin generates the following TypeScript files:
+
+- `schema.ts`: TypeScript object representation of the ZModel schema.
+- `models.ts`: Exports types for all models, types, and enums defined in the schema.
+- `input.ts`: Exports types that you can use to type the arguments passed to the ORM query API, such as `findMany`, `create`, etc.
+
+## Example
+
+```zmodel
+plugin ts {
+  provider = '@core/typescript'
+  output = '../generated'
+}
+```
diff --git a/versioned_docs/version-3.x/reference/zmodel-language.md b/versioned_docs/version-3.x/reference/zmodel-language.md
new file mode 100644
index 00000000..8b540c21
--- /dev/null
+++ b/versioned_docs/version-3.x/reference/zmodel-language.md
@@ -0,0 +1,8 @@
+---
+description: ZModel language references
+sidebar_position: 1
+sidebar_label: ZModel Language
+toc_max_heading_level: 3
+---
+
+# ZModel Language Reference
diff --git a/versioned_docs/version-3.x/roadmap.md b/versioned_docs/version-3.x/roadmap.md
new file mode 100644
index 00000000..0e9555f0
--- /dev/null
+++ b/versioned_docs/version-3.x/roadmap.md
@@ -0,0 +1,12 @@
+---
+sidebar_position: 11
+---
+
+# Roadmap
+
+- [ ] Access Control
+- [ ] Data Validation
+- [ ] Zod integration
+- [ ] Performance benchmark
+- [ ] Query-as-a-Service (automatic CRUD API)
+- [ ] Json filter
diff --git a/versioned_docs/version-3.x/samples.md b/versioned_docs/version-3.x/samples.md
new file mode 100644
index 00000000..2e2a6f1a
--- /dev/null
+++ b/versioned_docs/version-3.x/samples.md
@@ -0,0 +1,15 @@
+---
+sidebar_position: 8
+sidebar_label: Sample Projects
+---
+
+# Sample Projects
+
+You can find sample projects in the [samples](https://github.com/zenstackhq/zenstack-v3/tree/main/samples) folder of the GitHub repository. New samples will continue to be added.
+
+## Blog
+
+https://github.com/zenstackhq/zenstack-v3/tree/main/samples/blog
+
+A simple TypeScript application that demonstrates how to model a minimum blog app and how to use the ORM client to query the database.
+
diff --git a/versioned_docs/version-3.x/service/_category_.yml b/versioned_docs/version-3.x/service/_category_.yml
new file mode 100644
index 00000000..9f3e3b17
--- /dev/null
+++ b/versioned_docs/version-3.x/service/_category_.yml
@@ -0,0 +1,4 @@
+position: 5
+label: Query as a Service 🚧
+collapsible: true
+collapsed: true
diff --git a/versioned_docs/version-3.x/service/index.md b/versioned_docs/version-3.x/service/index.md
new file mode 100644
index 00000000..d97b5285
--- /dev/null
+++ b/versioned_docs/version-3.x/service/index.md
@@ -0,0 +1,3 @@
+# Overview
+
+Coming soon 🚧
\ No newline at end of file
diff --git a/versioned_docs/version-3.x/utilities/_category_.yml b/versioned_docs/version-3.x/utilities/_category_.yml
new file mode 100644
index 00000000..1fdb7623
--- /dev/null
+++ b/versioned_docs/version-3.x/utilities/_category_.yml
@@ -0,0 +1,7 @@
+position: 6
+label: Utilities
+collapsible: true
+collapsed: true
+link:
+    type: generated-index
+    title: Utilities
diff --git a/versioned_docs/version-3.x/utilities/zod.md b/versioned_docs/version-3.x/utilities/zod.md
new file mode 100644
index 00000000..a35591cd
--- /dev/null
+++ b/versioned_docs/version-3.x/utilities/zod.md
@@ -0,0 +1,8 @@
+---
+sidebar_position: 1
+description: Zod integration
+---
+
+# Zod 🚧
+
+Coming soon 🚧
diff --git a/versioned_docs/version-3.x/welcome.md b/versioned_docs/version-3.x/welcome.md
new file mode 100644
index 00000000..fa3bd385
--- /dev/null
+++ b/versioned_docs/version-3.x/welcome.md
@@ -0,0 +1,32 @@
+---
+description: Welcome to ZenStack
+slug: /welcome
+sidebar_label: Welcome
+sidebar_position: 1
+---
+
+# Welcome
+
+Welcome to ZenStack - the modern data layer for your TypeScript application!
+
+ZenStack is built with the belief that most applications should use the data model as their center pillar. If that model is well-designed, it can serve as the single source of truth throughout the app's lifecycle and be used to derive many other aspects of the app. The result is a smaller, more cohesive code base that scales well as your team grows while maintaining a high level of developer experience.
+
+Inside the package you'll find:
+
+- **Intuitive schema language**
+  
+  That helps you model data, relations, access control, and more, in one place. [🔗](./modeling/)
+
+- **Powerful ORM**
+  
+  With awesomely-typed API, built-in access control, and unmatched flexibility. [🔗](./orm/)
+
+- **Query-as-a-Service**
+  
+  That provides a full-fledged data API without the need to code it up. [🔗](./service/)
+
+- **Utilities**
+  
+  For deriving artifacts like Zod schemas, frontend hooks, OpenAPI specs, etc., from the schema. [🔗](./category/utilities)
+
+ZenStack originated as an extension to Prisma ORM. V3 is a complete rewrite that removed Prisma as a runtime dependency and replaced it with an implementation built from scratch ("scratch" = [Kysely](https://kysely.dev/) 😆). On its surface, it continues to use a "Prisma-superset" schema language and a query API compatible with PrismaClient. [This blog post](https://zenstack.dev/blog/next-chapter-1) contains more background about the thoughts behind the v3 refactor.
\ No newline at end of file
diff --git a/versioned_sidebars/version-3.x-sidebars.json b/versioned_sidebars/version-3.x-sidebars.json
new file mode 100644
index 00000000..fc4fe139
--- /dev/null
+++ b/versioned_sidebars/version-3.x-sidebars.json
@@ -0,0 +1,8 @@
+{
+  "mySidebar": [
+    {
+      "type": "autogenerated",
+      "dirName": "."
+    }
+  ]
+}
diff --git a/versions.json b/versions.json
index c339c072..bbcb99fe 100644
--- a/versions.json
+++ b/versions.json
@@ -1 +1 @@
-["1.x"]
+["1.x", "3.x"]
