Skip to content

Server Setup ​

The server side of nuxt-zenstack is powered by a ZenStack client that enforces access control policies defined in your ZModel schema. You need to initialize this client and provide it to the auto-generated endpoints on every request.

1. Define the auth type in your ZModel ​

Add an @@auth type so ZenStack knows the shape of the authenticated user:

prisma
// schema.zmodel

plugin policy {
    provider = "@zenstackhq/plugin-policy"
}

type AuthInfo {
    id   String
    role String
    @@auth
}

WARNING

  • The module works with Zenstack version 3 and is not compatible with version 2.
  • Data models should have a unique field named id, it can be a number or a string.
  • The ZenStack generated files should be located at the project root under the zenstack folder.

2. Initialize the database client ​

Create a server utility that initializes the ZenStack client and extends it with the policy plugin:

ts
// server/utils/db.ts
import { ZenStackClient } from '@zenstackhq/orm'
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite'
import Database from 'libsql'
import { schema } from '~/zenstack/schema'
import { PolicyPlugin } from '@zenstackhq/plugin-policy'

const db = new ZenStackClient(schema, {
  dialect: new SqliteDialect({
    database: new Database('_DATABASE_URL_'),
  }),
})

export const dbWithoutPolicy = extendZenstackClient(db)
export const dbWithPolicy = dbWithoutPolicy.$use(new PolicyPlugin())

NOTE

extendZenstackClient is automatically imported as a server utility. It wraps the client with the realtime mutation plugin when realtime: true is set in the module config.

3. Provide the client per request ​

Use a Nitro server plugin to inject the policy-enforced client on every incoming request. Call $setAuth with the current user's identity so that ZenStack can evaluate access policies:

ts
// server/plugins/zenstack.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', (event) => {
    // Replace with your actual auth session retrieval
    const client = dbWithPolicy.$setAuth({ id: 'user_id', role: 'user' })
    provideZenstackClient(event, client)
  })
})

IMPORTANT

provideZenstackClient must be called on every request. The auto-generated REST endpoints will use this client, which means access control is automatically applied.

4. Edge Workers ​

If you are deploying to an edge environment (like Cloudflare Workers), you will need to instantiate the database client per-request since edge workers cannot maintain long-lived connections outside the request lifecycle.

Here is an example using Neon Serverless with @neondatabase/serverless:

ts
// server/plugins/zenstack-edge.ts
import { ZenStackClient } from '@zenstackhq/orm'
import { schema } from '~~/zenstack/schema'
import { PolicyPlugin } from '@zenstackhq/plugin-policy'
import { Pool } from '@neondatabase/serverless'
import { PostgresDialect } from 'kysely'

export default defineNitroPlugin(async (nitroApp) => {
  const policyPlugin = new PolicyPlugin()

  nitroApp.hooks.hook('request', async (event) => {
    // 1. Create a new client per request using a serverless driver
    const db = new ZenStackClient(schema, {
      dialect: new PostgresDialect({
        pool: new Pool({ connectionString: '_DATABASE_URL_' })
      })
    })
    
    // 2. Wrap with the realtime (if enabled) and policy plugins
    const dbWithoutPolicy = extendZenstackClient(db)
    const dbWithPolicy = dbWithoutPolicy.$use(policyPlugin)

    // 3. Set auth context
    const client = dbWithPolicy.$setAuth({ id: 'user_id', role: 'user' })
    provideZenstackClient(event, client)

    // 4. Ensure the client disconnects when the request finishes
    event.waitUntil(client.$disconnect())
  })
})