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
}

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 available 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 or Vercel Edge Functions), you will need to instantiate the database client per-request since edge workers cannot maintain long-lived stateful TCP 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: process.env.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())
  })
})