> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Browser

> Run type-safe UQL queries from the browser, either against your API with HttpQuerier or against Postgres itself with PGlite.

Source: https://uql-orm.dev/browser

The browser can be either end of a UQL query. `uql-orm/browser` talks to your API over HTTP, and `uql-orm/pglite` skips the network by running Postgres in the tab.

## HTTP client

`uql-orm/browser` consumes the REST API served by the [HTTP core](https://uql-orm.dev/http.md) or the [Express adapter](https://uql-orm.dev/express.md), using the same query syntax you write on the server. It is optional, and it is an HTTP client rather than a driver: something on the other end still holds the connection.

```ts
import { HttpQuerier } from 'uql-orm/browser';
import { User } from './shared/models/index.js';

const querier = new HttpQuerier('https://api.yourdomain.com/api');

const { data: users } = await querier.findMany(User, {
  $select: { email: true },
  $populate: { profile: { $select: { picture: true } } },
  $where: { email: { $endsWith: '@domain.com' } },
  $sort: { createdAt: 'desc' },
  $limit: 10,
});
// typed User[], each with a typed profile
```

Entity classes are shared between backend and frontend, so the query type-checks identically on both sides.

What leaves the browser is JSON, so the client takes a [`WireQuery`](https://uql-orm.dev/querying/querier.md#the-same-query-every-transport): the query without [`raw`](https://uql-orm.dev/querying/raw-sql.md) SQL. A `raw` fragment or a binary value is refused rather than mangled; both belong in a route of your own.

### Client API

Every wire operation has a typed method: `findMany`, `findManyAndCount`, `findOne`, `findOneById`, `count`, `insertOne`, `insertMany`, `saveOne` (upsert via `PUT`), `saveMany`, `updateOneById`, `updateMany`, `deleteOneById`, `deleteMany`. Responses are `{ data, count? }`.

URLs derive from the shared `CRUD_ROUTES` contract in `uql-orm/http`, and a compile-time check guarantees the client covers every operation, so the mapping cannot drift from the server.

Failed requests throw a `RequestError` carrying the server’s message and the numeric HTTP `status`, so status-driven flows work without string matching:

```ts
import { RequestError } from 'uql-orm/browser';

try {
  await querier.findMany(User, {});
} catch (err) {
  if (err instanceof RequestError && err.status === 401) {
    location.href = '/login';
  }
}
```

### Options

```ts
// per instance: defaults that per-call options override
const querier = new HttpQuerier('/api', {
  headers: { Authorization: `Bearer ${session.token}` },
});

// per call: abort/timeout, headers, and `silent` to skip the notification bus
await querier.findMany(
  User,
  { $limit: 20 },
  {
    signal: AbortSignal.timeout(120_000),
    headers: { Authorization: `Bearer ${session.token}` },
  },
);
```

Build one scoped instance per server-side request rather than reusing a module-level one, so a token never leaks across requests.

For non-CRUD endpoints (`/api/payments/checkout`, …), the typed helpers `get`, `post`, `put`, `patch`, `remove` and `query` are exported too, sharing the same envelope, headers, notifications and `RequestError`.

### HTTP QUERY transport

Opt in to send read queries in the request body instead of the URL, which sidesteps URL-length limits on large `$where`/`$populate`:

```ts
const querier = new HttpQuerier('/api', { readMethod: 'QUERY' });
```

`findOne`, `findMany` and `count` then use [`QUERY`](https://uql-orm.dev/http.md#http-query-rfc-10008); writes and by-id reads keep their canonical methods. The default stays `GET` because cross-origin `QUERY` needs a CORS preflight and some proxies still drop the method. The server accepts both at once, so this is a per-client switch.

### Request notifications

A small pub/sub bus (`on`) emits `start`, `success`, `error` and `complete` per request, which is enough for a global spinner in a vanilla app. Libraries that already track loading state do not need it: pass `{ silent: true }`. See the [TanStack Query recipe](https://uql-orm.dev/react-query.md), where the serializable query doubles as the cache key.

## Postgres in the tab

[PGlite](https://uql-orm.dev/pglite.md) is Postgres compiled to WASM, and `idb://` is its browser `dataDir`, so `PgliteQuerierPool` runs in a page with no server behind it. Same entities, same queries, same migrations as the server, because it reports itself as the `postgres` dialect:

```ts
import { PgliteQuerierPool } from 'uql-orm/pglite';
import { User } from './shared/models/index.js';

const pool = new PgliteQuerierPool('idb://app');
const users = await pool.findMany(User, {
  $select: { id: true, email: true },
});
```

The database survives a reload: `idb://` persists into IndexedDB.

Two things your bundler has to get right, each with its own failure:

- **Emit PGlite’s assets.** It fetches `pglite.wasm`, `initdb.wasm` and `pglite.data` from the page’s origin at runtime. When they 404, the first query throws `Invalid FS bundle size`.
- **Set a dated `target`.** No browser implements decorators natively, so `esnext` leaves the syntax in and the page dies on `Invalid or unexpected token` before any of your code runs. Same [requirement](https://uql-orm.dev/getting-started.md) as on the server.

One connection means one transaction at a time, which for a single tab is rarely the limit it sounds like. [PGlite](https://uql-orm.dev/pglite.md#one-connection-and-what-follows-from-it) has the rest.
