# Repository

> A typed repository per table, compiled to one PostgREST request.

Source: https://bettersupabase.com/docs/repository

```ts
const betterSupabase = defineSupabase(schema);
const db = betterSupabase.connect(supabase); // any SupabaseClient, or an Executor
```

`db.<table>` is created lazily for every table and view in the schema. Views
get the read methods only.

## Reading [#reading]

| Method                  | Returns                                                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `findMany(args?)`       | `Row[]`                                                                                                    |
| `findFirst(args?)`      | `Row \| null`                                                                                              |
| `findOnly({ where })`   | `Row \| null`, or a `multiple_rows` error when more than one row matches                                   |
| `findById(key, args?)`  | `Row`, or a `not_found` error                                                                              |
| `count({ where? })`     | `number` (a `HEAD` request)                                                                                |
| `exists({ where? })`    | `boolean`                                                                                                  |
| `findUnique({ where })` | `Row \| null`, by the primary key or any unique key; see [unique keys](/docs/repository/unique-and-errors) |
| `paginate(args)`        | A page; see [pagination](/docs/repository/pagination)                                                      |

`findOnly` is for a filter that should match at most one row but has no
unique key behind it, such as the active subscription of a customer. It
asks for two rows and returns a `multiple_rows` error (status 409) when it
gets both, where `findFirst` would return one of them. It returns `null`
when nothing matches, like supabase-js `maybeSingle()`.

Every read takes `select`, `where`, `include`, `orderBy`, `limit`, `offset`
and `signal`. `limit` and `offset` must be non-negative integers; anything
else returns `invalid_request` before a request is sent. The result type follows `select` and `include`:

```ts
const row = await db.customers
  .findById(id, {
    select: ["name"],
    include: { notes: { select: ["body"], limit: 3 } },
  })
  .orThrow();
// { name: string; notes: { body: string }[] }
```

Composite keys take an object: `db.customerTags.findById({ customerId, tagId })`.

## Timeouts and retries [#timeouts-and-retries]

Every read, write and `$rpc` call also takes `timeout` and `retry`:

```ts
const rows = await db.customers.findMany({
  where: { status: "lead" },
  timeout: 2000,
  retry: false,
});
```

`timeout` is in milliseconds. It is combined with `signal` (either one aborts
the request), and a call that runs past it fails with `timeout` (504) instead
of `aborted`. `retry` turns supabase-js retries on or off for the call: they
repeat `GET` and `HEAD` requests after a network error, a 503 or a 520, and
never repeat a write. Set both for a whole connection with
`betterSupabase.connect(client, context, { timeout, retry })`; a call's own
values win.

On the [Postgres executor](/docs/auth/postgres), `timeout` only stops a call
that has not started its statement yet, so use `statement_timeout` for long
queries, and `retry` does nothing.

## Escape hatches [#escape-hatches]

* `db.$client` is the client you passed in.
* `db.$rpc('fn', args, { returns })` calls a typed Postgres function, and can
  validate the result with a Standard Schema. Rows of a table and
  `returns table (...)` records come back in the configured
  [casing](/docs/concepts/casing#function-results); `{ raw: true }` keeps
  database names.
* `db.$with({ signal })` returns a Db bound to a new request context.
* `db.$table(name)` returns the repository for a table key known only at
  runtime, and throws a `TypeError` for unknown names.
* `db.$withoutPlugins()` returns the same connection without any plugins,
  for migrations and admin scripts. `db.$withoutPlugins({ keep: ['otel'] })`
  keeps the named plugins, such as tracing.
* `db.$run(spec)` runs a serializable [query spec](/docs/concepts/caching#query-specs).
* `db.$many([specA, specB])` runs specs together and returns a tuple;
  `db.$many(readSet, params)` runs a [read set](/docs/repository/read-sets)
  as one request.
* `db.$stats()` returns `{ calls, waves, tables, ms }` for everything this
  `db` (and its `$with` copies) ran. `betterSupabase.connect(client, context, { stats })`
  also reports into a shared `StatsRecorder`. See
  [Budget](/docs/frameworks/next-cache-components#budget).
* `explainPostgrest(op)` shows the compiled request for debugging.

## How queries compile [#how-queries-compile]

A call compiles to an intermediate representation, which a compiler turns
into a PostgREST request. Nothing runs through string templates you write:
values are quoted, and LIKE input used by `contains`, `startsWith` and
`endsWith` is escaped. A filter that can never match (`{ in: [] }`) skips
the request.