> 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

# Supabase

> Run UQL on Supabase Postgres, pick the right pooler endpoint, and layer UQL security filters over Postgres RLS.

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

Supabase is Postgres, so everything on the [PostgreSQL](https://uql-orm.dev/postgres.md) page applies unchanged: same entities, queries, migrations and `pg` driver. What is Supabase-specific is which endpoint you connect to and how UQL’s tenant scoping lines up with RLS.

```sh
npm install uql-orm pg
```

## Which connection string

Postgres holds session state (prepared statements, `SET`, advisory locks, temp tables) on a connection, and a transaction-mode pooler hands you a different backend connection between statements. So the endpoint is a real choice:

| Endpoint | Port | Use it for |
| - | - | - |
| `db.<ref>.supabase.co` | 5432 | Migrations, `pg_dump`, long-lived servers on an IPv6-capable network. |
| `aws-<region>.pooler.supabase.com` (session mode) | 5432 | The same, from an IPv4-only network. |
| `aws-<region>.pooler.supabase.com` (transaction mode) | 6543 | Serverless and anything opening many short-lived connections. |

The direct hostname resolves to IPv6 only unless you buy the IPv4 add-on, which is the usual cause of `ENETUNREACH` from a CI runner or a container with no IPv6 route.

```ts
import { PgQuerierPool } from 'uql-orm/postgres';

export const pool = new PgQuerierPool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
});
```

Transaction mode rejects prepared statements, since the statement would be prepared on one backend connection and executed on another. UQL issues none of its own, so the common paths work; if you enable them in `pg`, turn them off for that endpoint.

That is also why [migrations](https://uql-orm.dev/migrations.md) belong on the direct endpoint. Point the migrator at it explicitly:

```ts title="uql.config.ts"
import type { Config } from 'uql-orm';
import { PgQuerierPool } from 'uql-orm/postgres';
import { Company, Invoice } from './entities.js';

export default {
  pool: new PgQuerierPool({
    connectionString: process.env.DIRECT_DATABASE_URL,
  }),
  entities: [Company, Invoice],
  migrationsPath: './migrations',
} satisfies Config;
```

## Row-level security

Your RLS policies are written against `auth.uid()` and `auth.jwt()`, which the PostgREST layer populates per request. Connecting with `pg` bypasses that layer: you are the `postgres` role, policies do not apply, and nothing scopes your queries. There are two ways to get scoping back, and they stack.

A **UQL security filter** is the boundary in application code. It is AND-merged into every query the ORM generates, cannot be turned off from the wire, and fails closed when the context is missing:

```ts
import { Entity, Filter } from 'uql-orm';

@Filter('tenant', {
  where: (ctx) =>
    ctx?.orgId != null ? { organizationId: ctx.orgId } : undefined,
  security: true,
})
@Entity()
export class Invoice {}
```

Set the context once per request and every read, write, relation and cascade is scoped. See [Multi-tenancy](https://uql-orm.dev/multi-tenancy.md).

**Postgres RLS** is the backstop underneath, and it covers raw SQL and anything else that skips the ORM. Connect as a role the policies apply to, and set the claim inside the transaction that reads it:

```ts
await pool.transaction(async (querier) => {
  await querier.run("SELECT set_config('request.jwt.claims', $1, true)", [
    JSON.stringify(claims),
  ]);
  return querier.findMany(Invoice, { $limit: 50 });
});
```

The third argument is what scopes the setting to the transaction. Drop it and the claim stays on the connection, so the next request to borrow it inherits the previous caller’s identity.

## What you keep

UQL replaces `supabase-js` for data access only. Auth, Storage, Realtime and Edge Functions talk to their own endpoints and keep working; a common shape is Supabase Auth issuing the JWT, your server verifying it, and the verified claims becoming the UQL context above. pgvector is installed on every project, so [semantic search](https://uql-orm.dev/querying/semantic-search.md) needs nothing beyond the [index](https://uql-orm.dev/entities/indexes.md) your migration creates.

Edge Functions run on Deno with no TCP, so `pg` cannot connect there. For pool placement in functions that freeze, see [Serverless](https://uql-orm.dev/serverless.md).
