> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Database client

> The typed zitejs/db client: findAll, findOne, create, update, delete, bulkCreate, and read-only SQL with zite.sql().

Every app gets a typed database client, generated from the workspace schema. Import it from `zitejs/db`:

```typescript theme={null}
import { zite } from 'zitejs/db';
```

The client is used inside backend [workflows](/framework/workflows) — the frontend never talks to the
database directly. Each table is a property on `zite`, addressed by its **SDK name** (PascalCase), with
fully-typed record types. Read the generated `.zite/db.ts` to see the exact names and types.

## Methods

```typescript theme={null}
// Query many rows
const { records, hasMore } = await zite.Tickets.findAll({
  filter: { status: 'open' },
  sort: [{ field: 'createdAt', direction: 'desc' }],
  limit: 100,
  offset: 0,
});

// One row by id (or by filter)
const ticket = await zite.Tickets.findOne({ id: 'rec_123' });
const byEmail = await zite.Customers.findOne({ filters: { email: 'a@b.com' } });

// Create, update, delete
const created = await zite.Tickets.create({ record: { subject: 'Help', status: 'open' } });
const updated = await zite.Tickets.update({ id: created.id, record: { status: 'closed' } });
await zite.Tickets.delete({ id: created.id });

// Insert many (optionally upsert on matching fields)
const result = await zite.Customers.bulkCreate({
  records: [{ email: 'a@b.com' }, { email: 'c@d.com' }],
  matchOn: ['email'],
});
```

| Method       | Arguments                                      | Returns                    |
| ------------ | ---------------------------------------------- | -------------------------- |
| `findAll`    | `{ limit?, offset?, sort?, filter?, fields? }` | `{ records, hasMore }`     |
| `findOne`    | `{ id? }` or `{ filters? }` (+ `fields?`)      | the record, or `undefined` |
| `create`     | `{ record }`                                   | the created record         |
| `update`     | `{ id, record }`                               | `{ id, fields }`           |
| `delete`     | `{ id }`                                       | `{ id }`                   |
| `bulkCreate` | `{ records, matchOn? }`                        | `{ success, records }`     |

## Filters and sorting

Filters are objects keyed by field SDK name. A bare value means equality; an object applies an operator:

```typescript theme={null}
{ status: 'open' }                      // equality
{ createdAt: { gt: '2026-01-01' } }     // operator
```

| Operator       | Meaning               | Example                               |
| -------------- | --------------------- | ------------------------------------- |
| *(bare value)* | Equals                | `{ status: 'open' }`                  |
| `contains`     | Substring match       | `{ subject: { contains: 'refund' } }` |
| `gt`           | Greater than          | `{ createdAt: { gt: '2026-01-01' } }` |
| `gte`          | Greater than or equal | `{ priority: { gte: 3 } }`            |
| `lt`           | Less than             | `{ createdAt: { lt: '2026-02-01' } }` |
| `lte`          | Less than or equal    | `{ priority: { lte: 2 } }`            |

<Note>
  There is no `eq` or `neq` here — equality is the bare-value shorthand. The row filters in
  [`zite.permissions.json`](/framework/permissions-file) are a separate system and *do* have `eq` and `neq`.
</Note>

Sort is an array: `sort: [{ field: 'createdAt', direction: 'desc' }]`. Use `fields: ['subject', 'status']`
to fetch only the columns you need.

<Warning>
  `findAll` returns at most **2,000 records** (`hasMore` tells you there are more). For counts, sums,
  group-bys, and joins, use `zite.sql()` — aggregating in JavaScript over a capped result set gives wrong
  numbers on large tables.
</Warning>

## Read-only SQL

`zite.sql()` runs a single read-only `SELECT` against the workspace database using human-readable SDK names:

```typescript theme={null}
const { rows, rowCount, truncated } = await zite.sql({
  query: `
    SELECT "status", COUNT(*) AS n
    FROM "Tickets"
    WHERE "createdAt" > $1
    GROUP BY "status"
  `,
  params: ['2026-01-01'],
});
```

Always use SDK names, never display labels. A wrong name is a runtime error, not a type error:

| Identifier           | Convention                         | Quote?   | Example                                    |
| -------------------- | ---------------------------------- | -------- | ------------------------------------------ |
| Table                | PascalCase SDK name                | Yes      | `"Tickets"`                                |
| Field                | camelCase SDK name                 | Yes      | `"assigneeEmail"`                          |
| System column        | lowercase                          | Optional | `id`, `created_at`, `updated_at`           |
| Link table           | Both table SDK names, alphabetical | Yes      | `"Tickets"` + `"Users"` → `"TicketsUsers"` |
| Link table id column | camelCase table name + `Id`        | Yes      | `"ticketsId"`, `"usersId"`                 |

Postgres lowercases unquoted names, which is why everything but the system columns must be double-quoted.
The `.zite/db.ts` header lists the link tables for your workspace.

<Warning>
  `SELECT` only. Pass runtime values through `params` (`$1`, `$2`, …) — never string-interpolate them into
  the query. Soft-deleted rows are excluded automatically.
</Warning>

Results are capped at 2,000 rows; `truncated` is `true` when the cap is hit.

## Also on the client

The generated `zite` object also exposes `zite.notifications` (see [Notifications](/framework/utilities#notifications))
and `zite.meta.listUsers()` for the workspace's users.
