Skip to content
NewFrom Mongoose to SQL: migrating MongoDB to PostgreSQL7 min read

Supabase

Supabase is Postgres, so the PostgreSQL page applies unchanged. What is specific to Supabase is which endpoint you connect to and how tenant scoping meets RLS.

Terminal window
npm install uql-orm pg
Endpoint Port Use it for
db.<ref>.supabase.co 5432 Migrations, pg_dump, long-lived servers with IPv6
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 many short-lived connections

The direct hostname is IPv6-only unless you buy the IPv4 add-on, which is the usual cause of ENETUNREACH from CI or a container.

import { PgQuerierPool } from 'uql-orm/postgres';
export const pool = new PgQuerierPool({
connectionString: process.env.DATABASE_URL,
max: 10,
});

Transaction mode hands you a different backend connection between statements, so session state does not survive and prepared statements fail. UQL prepares none of its own; if you enable them in pg, turn them off for that endpoint.

Run migrations on the direct endpoint:

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;

RLS policies read auth.uid() and auth.jwt(), which Supabase’s API layer fills. Connecting with pg skips that layer: as the postgres role, no policy applies. Two layers restore scoping, and they stack.

A UQL security filter is AND-merged into every query the ORM builds, cannot be turned off from the wire, and fails closed when the context is missing:

import { Entity } from 'uql-orm';
@Entity({
filters: {
tenant: {
where: (ctx) =>
ctx?.orgId != null ? { organizationId: ctx.orgId } : undefined,
security: true,
},
},
})
export class Invoice {}

RLS is the backstop for raw SQL and anything else that skips the ORM. Connect as a role the policies apply to, and set the claims inside the transaction:

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

UQL replaces supabase-js for data access only; Auth, Storage and Realtime keep working, and verified Auth claims can become the UQL context. pgvector is installed on every project, so semantic search needs only the index your migration creates.

Edge Functions run on Deno without TCP, so pg cannot connect there. For functions that freeze between invocations, see Serverless.