Skip to main content
Some SQL doesn’t fit PostgREST: schema introspection, ad-hoc queries during development, bulk imports, scheduled jobs. For those, connect directly to Postgres via the Database URL from the Connect modal. This guide covers the patterns that work, focused on the constraints of transaction-mode pooling. For the pooler-level concerns (what breaks in transaction mode, per-driver flags), see Connection pooling. For ORM-specific setup, see the four ORM pages: Prisma, Drizzle, SQLAlchemy, TypeORM. For migration workflows, see Migrations.

The Database URL

Copy from the Connect modal in the Studio:
Username and database are both your project ref, not postgres. This is the PgBouncer pooler URL; there is no separate direct (non-pooled) endpoint exposed externally.

psql from your laptop

You’re connected as supabase_admin, with full schema ownership across public, ai, auth, storage, and extensions. Treat this like database root access. The Database URL is a secret; never commit it, never paste it in chat. Useful first queries to orient yourself:

Transactions inside the pooler

PgBouncer’s transaction mode means a server connection is held for the duration of one transaction, then returned to the pool. The implication for your code: if you want a sequence of statements to share state (a temp table, a SET LOCAL, a prepared statement), wrap them in a transaction. Outside a transaction, every statement potentially lands on a different server connection, so you can’t rely on session-scoped state.
For psql sessions this is rarely an issue, since psql opens a persistent connection and your statements stay on it. The pooler’s transaction-mode quirks bite hardest when your driver opens new connections per query, or when a connection pool in your app rotates connections out from under you.

Common SQL patterns

A few patterns that come up in practice.

Bulk import from a CSV

The \copy meta-command (not the SQL COPY) reads from your local filesystem. ON COMMIT DROP cleans up the temp table even though it’s pooler-friendly (the whole flow is in one transaction).

Renaming a column without breaking PostgREST clients

For zero-downtime, the safer sequence is to add the new column, dual-write from triggers, migrate readers, then drop the old column. The single ALTER is fine for development; production schema changes deserve more care.

Reading a large result without buffering

FETCH_COUNT makes psql page through the result rather than buffering it all in memory. Useful for “what does this look like across millions of rows” exploration.

Cancelling a runaway query

If you start a query and want to stop it from another session:
pg_cancel_backend is the right first move; pg_terminate_backend is the escalation if cancel doesn’t work.

Error classes worth knowing

Postgres errors come with a 5-character SQLSTATE class. The classes you’ll see most:
SQLSTATENameWhen
23505unique_violationInserted a duplicate value for a unique index, including primary key collisions
23503foreign_key_violationInserted a row referencing a non-existent parent, or deleted a row with children
23502not_null_violationMissed a NOT NULL column on insert
23514check_violationA CHECK constraint failed
40001serialization_failureConflicting concurrent transactions at SERIALIZABLE isolation. Retry the transaction.
40P01deadlock_detectedTwo transactions waiting on each other; Postgres killed one. The killed transaction should retry.
42501insufficient_privilegeTried to act on something you don’t have permissions for (rare as supabase_admin)
42883undefined_functionCalled a function with wrong argument types, often a cast issue
42P01undefined_tableTable doesn’t exist (or you forgot Accept-Profile/search_path for ai.*)
26000invalid_sql_statement_namePgBouncer footgun. Your prepared statement isn’t on this server connection. See Connection pooling.
08006connection_failureConnection dropped. Almost always retryable.
Most drivers expose SQLSTATE as a structured property. In Python psycopg:
The retry pattern for 40001 and 40P01 is “back off briefly, redo the entire transaction.” Half-retrying a multi-statement transaction is rarely what you want, since the partial state is already gone.

Connection lifecycle from a backend

For application servers (not psql), the right shape depends on your runtime: Long-running services (Node, Python, Go on Kubernetes / VMs):
  • One client-side pool per process, sized at 10-15 connections.
  • Each request acquires a connection, runs its work (inside a transaction if it does more than one statement), returns the connection.
  • Keep the pool alive for the lifetime of the process.
Serverless (Lambda, Vercel Functions, Cloudflare Workers):
  • One connection per invocation. Open, transact, close.
  • Don’t try to reuse a connection across invocations; the runtime tears down state unpredictably, and you’ll leak server connections at PgBouncer.
  • Or use the HTTP-friendly PostgREST API instead. It’s stateless and doesn’t burn pooler slots.
One-off scripts (migrations, batch jobs):
  • One connection, in a single transaction, then exit.
  • For long migrations that hit transaction-mode constraints, see Migrations.

When to use direct Postgres vs PostgREST vs typed API

A rough decision tree:
GoalUse
CRUD on public.* with RLS, called from clientsPostgREST /rest/v1/*
Bulk INSERT/UPDATE/DELETE from a backendDirect Postgres (faster than N PostgREST calls)
Schema changes (CREATE TABLE, ADD COLUMN, etc.)Direct Postgres
Read-only queries with complex JOINs / aggregationsDirect Postgres or PostgREST RPC
AI features (run agent, search KB, upload source)Typed /api/*
Custom analytics on ai.* tablesPostgREST with Accept-Profile: ai
Pub/sub-style notificationsRealtime (not LISTEN/NOTIFY through the pooler)
The typed /api/* is the only path for AI operations, and PostgREST is the right path for client-side CRUD. Direct Postgres covers everything else, especially anything that touches schema or runs many statements.

Next steps

Connection pooling

The lower-level constraints that shape what works at the connection level.

Migrations

Schema evolution patterns for hand-written SQL, Drizzle, and Prisma migrations.

Prisma

The TypeScript ORM most teams reach for.

SQLAlchemy + Alembic

The dominant Python ORM with the migration runner Powabase teams typically use.