Why Vercel-Neon integration uses NOLOGIN roles in preview connection URLs

Agustin Crovetto
Tue, 10 Mar 2026 17:56:56 +0000
Help

Problem

After running a Prisma migration that creates a new PostgreSQL role with NOLOGIN for Row-Level Security (RLS) purposes, the Neon-Vercel integration for preview deployments started providing connection URL environment variables (DATABASE_URL / POSTGRES_URL) that reference this new NOLOGIN role instead of the original default owner role.

This causes all preview deployment builds to fail at the prisma migrate deploy step with:

Error: P1001: Can’t reach database server at ep-XXXXX.us-east-1.aws.neon.tech:5432

The NOLOGIN role cannot be used to authenticate — it is designed to be assumed via SET LOCAL ROLE inside transactions, never for direct connections. The integration should not be selecting it as the connection role.

Expected Behavior

The integration should continue using the original default role (the Neon owner role) for connection URLs in preview branches, regardless of additional roles created via migrations. At minimum, a role with NOLOGIN should never be selected as the connection role.

Steps to Reproduce

1. Have the Neon-Vercel integration set up with preview environments (separate Neon branch per Vercel preview deployment).

2. Run a migration that creates a new PostgreSQL role with NOLOGIN and grants it table-level permissions:

DO $$ BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tenant_user_role') THEN
    CREATE ROLE tenant_user_role NOLOGIN;
  END IF;
END $$;

GRANT USAGE ON SCHEMA public TO tenant_user_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tenant_user_role;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO tenant_user_role;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO tenant_user_role;

3. The same migration also enables RLS on tables and creates policies (policies apply to {public}, i.e., all roles).

4. Push a new git branch to trigger a Vercel preview deployment.

5. The Neon integration creates a new preview branch — but the connection URL environment variables now use tenant_user_role instead of default.

6. Build fails because tenant_user_role has NOLOGIN and cannot authenticate.

What I’ve Verified

The injected env var is the problem — the connection URL provided by the integration contains the tenant_user_role. Manually replacing it with the original default role and password resolves the issue immediately.

Table and schema ownership is correct — all tables in public are owned by default, the schema is owned by default, and the database ACLs show default as the owner:

SELECT tablename, tableowner FROM pg_tables WHERE schemaname = 'public';
-- All rows show: tableowner = "default"

SELECT nspname, pg_catalog.pg_get_userbyid(nspowner) AS owner
FROM pg_namespace WHERE nspname = 'public';
-- owner = "default"

The role is correctly NOLOGIN — SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'tenant_user_role' confirms rolcanlogin = false.

The database is reachable — connecting via the Neon serverless driver (HTTP) with SELECT 1 succeeds.

Production deployments are unaffected — they use a separate Neon project.

No role-level defaults were set — the migration does not alter pg_db_role_setting.

Why the New Role Exists

The tenant_user_role is part of implementing Row-Level Security (RLS) for multi-tenant isolation. The application authenticates as the Neon owner role (default) and then assumes tenant_user_role via SET LOCAL ROLE within individual transactions. This is a standard PostgreSQL pattern.

Environment

Framework: Next.js 15 (App Router)

ORM: Prisma 6.8.0

Database: Neon Serverless PostgreSQL

Deployment: Vercel with Neon integration for preview environments

What I’ve Tried

Manually overriding the connection URL with the default role and password — this works.

Adding a keep-alive script using @neondatabase/serverless to ping the database via HTTP during builds.

Redeploying multiple branches — all new preview branches exhibit the same behavior.

Questions for the Neon Team

1. Why does the integration select a NOLOGIN role for the connection string? Shouldn’t rolcanlogin = false disqualify a role?

2. Is there a way to explicitly configure which role the integration uses for preview branch connection URLs?

3. Is this a known issue with the integration’s role detection logic when RLS is enabled?

2 posts - 2 participants