# Limitations

> What PostgREST can't do, and what better-supabase does instead.

Source: https://bettersupabase.com/docs/guides/limitations

Most apps talk to Supabase over PostgREST. It's the right default, and the
reason some things you'd expect from an ORM aren't available there. When
they matter, use a direct connection from `better-supabase/postgres`; the
repository API stays the same.

## No transactions over PostgREST [#no-transactions-over-postgrest]

Every PostgREST request is its own transaction. Two calls can't commit
together, and a nested write (`create` with related rows) is several
requests.

Instead:

* Put multi-step writes in a database function and call it with
  `db.$rpc()`. Declare what it changes with
  [`betterSupabase.defineRpc`](/docs/concepts/caching#writes-invalidation-targets) so
  caches refresh.
* On the server, use `postgres.transaction()` with
  [`postgresExecutor`](/docs/auth/postgres). The same `db` calls then run in
  one transaction, as the user or as admin.

## Relation filters on writes [#relation-filters-on-writes]

`where: { notes: { some: ... } }` works on reads. PostgREST can't filter an
`update` or `delete` by a related table, so those fail with
`invalid_request` before a request is sent. Read the keys first, or use a
database function or a direct connection, where the same `where` compiles
to `exists (...)`.

## `every` on to-many relations [#every-on-to-many-relations]

`every` compiles to "no row that fails the condition", which is correct for
empty relations too. `every: {}` is always true and is dropped. Over
PostgREST it costs an embedded anti-join per condition; for large relations,
prefer a column or a view that stores the answer.

## Counts [#counts]

`_count` includes and `count: 'exact'` run `count(*)` under RLS. On large
tables use `count: 'estimated'` for pagination, and only count the
relations you show.

## Jobs over PostgREST [#jobs-over-postgrest]

The [jobs block](/docs/blocks/jobs) prefers a direct connection. Its PostgREST
transport uses the `pgmq_public` functions, which can send, read and
archive. It can't:

* deduplicate: `dedupeKey` fails with `invalid_request`,
* schedule or unschedule cron jobs: both fail with `invalid_request`,
* extend a lease: `extend` returns `false`, so heartbeats do nothing,
* record `lastError` or back off: a failed job reappears when its lease
  runs out, and is archived as dead after `maxAttempts`.

Workers that need these run on the server with `createPostgres()`.

## Acting as a user without Postgres [#acting-as-a-user-without-postgres]

`bs.forContext` and `bs.actingAs` run work as a user who isn't calling, for
jobs and webhooks. They need a direct Postgres connection, because they set
the user's claims for the transaction themselves. The Data API can't do
that: PostgREST only trusts tokens signed by the project's signing keys,
and with asymmetric keys only Supabase Auth holds the private key, so the
app can't mint a token for the user (see
[Why only over Postgres](/docs/auth/impersonation#why-only-over-postgres)).

An Edge Function with only the Data API can therefore act as its caller, but
not as anyone else. Run user-scoped workers where a
[pooler URL](/docs/auth/postgres#which-connection-string) is available, or
keep the work inside the user's own request. Don't switch to the service
role to get around it: that bypasses RLS for the whole job.

## Live queries [#live-queries]

[Live queries](/docs/frontend/live-queries) refetch the whole query on a
change, one request per affected query. They don't patch rows in place and
don't see tables outside `realtime.tables`.

## Generated types [#generated-types]

`gen` needs the database: a local connection, or the Management API for a
hosted project. It can't infer types from migrations alone. Commit the
snapshot and run `better-supabase gen --check` in CI.