ZQL on the Server

The Zero package includes utilities to run ZQL on the server directly against your upstream Postgres database.

This is useful for many reasons:

  • It allows Custom Mutators to read data using ZQL to check permissions or invariants.
  • You can use ZQL to implement standard REST endpoints, allowing you to share code with custom mutators.
  • In the future (but not yet implemented), this can support server-side rendering.

Creating a Database

To run ZQL on the database, you will create a ZQLDatabase instance. Zero ships with several built-in factories for popular Postgres bindings libraries.

node-postgres

The industry standard node-postgres library is supported via zeroNodePg:

import {Pool} from 'pg';
import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg';
import {schema} from '../shared/schema';

const db = zeroNodePg(schema, new Pool({
  connectionString: process.env.ZERO_UPSTREAM_DB!,
}));

You can also pass a Client:

import {Client} from 'pg';
import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg';
import {schema} from '../shared/schema';  
const client = new Client({
  connectionString: process.env.ZERO_UPSTREAM_DB!,
});
await client.connect();
const db = zeroNodePg(schema, client);

Postgres.js

The popular Postgres.js library is supported via zeroPostgresJS:

import postgres from 'postgres';
import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs';
import {schema} from '../shared/schema';

const db = zeroPostgresJS(
  schema,
  postgres(process.env.ZERO_UPSTREAM_DB!),
);

Drizzle

Zero also includes an adapter for Drizzle ORM:

import {Pool} from 'pg';
import {drizzle} from 'drizzle-orm/node-postgres';
import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle';
import * as drizzleSchema from './drizzle-schema';

const drizzleDb = drizzle(pool, { schema: drizzleSchema });
const db = zeroDrizzle(schema, drizzleDb);

Within your custom mutators, you can access the underlying drizzleDb via tx.wrappedTransaction:

// server-mutators.ts
// Use a type helper for the server transaction type
import type {ServerTransaction} from '@rocicorp/zero/server';
import type {DrizzleTransaction} from '@rocicorp/zero/server/adapters/drizzle';

type ServerTx = ServerTransaction<
  Schema,
  DrizzleTransaction<typeof drizzleDb>
>;

async function createUser(
  tx: ServerTx,
  { id, name }: { id: string; name: string },
) {
  // this is then fully typed w/ drizzle
  await tx.dbTransaction.wrappedTransaction
    .insert(drizzleSchema.user)
    .values({ id, name });
}

Custom Database

To implement support for some other Postgres bindings library, you will implement the DBConnection interface.

See the implementations for the existing adapters for examples.

Running ZQL

Once you have an instance of ZQLDatabase, use the transaction() method to run ZQL:

await db.transaction(async tx => {
  // await tx.mutate...
  // await tx.query...
  // await myMutator(tx, ...args);
});

SSR

Zero doesn't yet have the wiring setup in its bindings layers to really nicely support server-side rendering (patches welcome though!).

For now, we don't recommend using Zero with SSR. Use your framework's recommended pattern to prevent SSR execution:

Next.js

Add the use client directive.

SolidStart

Wrap components that use Zero with the clientOnly higher-order component.

The standard clientOnly pattern uses dynamic imports, but note that this approach (similar to React's lazy) works with any function returning a Promise<{default: () => JSX.Element}>. If code splitting is unnecessary, you can skip the dynamic import.

TanStack Start

Use React's lazy for dynamic imports.