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:
// 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:
// 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:
// 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:
// 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())
})
})