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.