Neon Postgres Security Guide for Vibe Coders
Published on January 23, 2026 - 12 min read
TL;DR
Neon provides serverless Postgres with branching and autoscaling. Secure it by using connection pooling for serverless functions, implementing Row Level Security (RLS) for multi-tenant apps, creating separate database roles with minimal permissions, and never exposing your connection string. Neon's branching is powerful for development but requires careful credential management across branches.
Why Neon Security Matters for Vibe Coding
Neon is a serverless Postgres platform that separates storage and compute, allowing instant branching and autoscaling. When AI tools generate database code for Neon, they often miss critical security patterns like Row Level Security, proper connection pooling configuration, and role-based access control.
Since Neon gives you full Postgres, you have access to powerful security features like RLS that serverless databases like Supabase already implement by default. The key is knowing how to use them.
Connection String Security
Neon provides connection strings that include credentials. Handle them carefully.
Connection String Types
# Direct connection (for long-running processes)
DATABASE_URL="postgres://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"
# Pooled connection (for serverless functions)
DATABASE_URL="postgres://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
When to Use Pooled Connections
Serverless functions (Vercel, Cloudflare Workers, AWS Lambda) should always use the pooled connection string. The pooler URL contains -pooler in the hostname. Direct connections from serverless functions can exhaust connection limits and cause failures.
Environment Configuration
# .env.local (never commit)
DATABASE_URL="postgres://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/neondb"
# For migrations (needs direct connection)
DIRECT_DATABASE_URL="postgres://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb"
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_DATABASE_URL")
}
Row Level Security (RLS)
RLS is the most powerful security feature Postgres offers. It enforces access control at the database level, making it impossible to bypass even if your application code has bugs.
Basic RLS Setup
-- Enable RLS on a table
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Create a policy for tenant isolation
CREATE POLICY tenant_isolation ON documents
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- Force RLS even for table owners
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
Setting Context in Your Application
// With Prisma and raw SQL for setting context
async function withTenant<T>(
tenantId: string,
operation: () => Promise<T>
): Promise<T> {
// Set the tenant context
await prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;
// Execute the operation
return operation();
}
// Usage
const documents = await withTenant(user.tenantId, () =>
prisma.document.findMany()
);
// RLS automatically filters to only this tenant's documents
User-Based RLS
-- Policy for user ownership
CREATE POLICY user_owns_row ON user_data
FOR ALL
USING (user_id = current_setting('app.current_user')::uuid);
-- Separate policies for different operations
CREATE POLICY users_can_read ON posts
FOR SELECT
USING (published = true OR author_id = current_setting('app.current_user')::uuid);
CREATE POLICY users_can_insert ON posts
FOR INSERT
WITH CHECK (author_id = current_setting('app.current_user')::uuid);
CREATE POLICY users_can_update ON posts
FOR UPDATE
USING (author_id = current_setting('app.current_user')::uuid);
CREATE POLICY users_can_delete ON posts
FOR DELETE
USING (author_id = current_setting('app.current_user')::uuid);
Database Roles and Permissions
Create roles with minimal necessary permissions instead of using the default superuser role.
Role Hierarchy
-- Create application role (limited permissions)
CREATE ROLE app_user WITH LOGIN PASSWORD 'secure_password';
-- Grant only necessary permissions
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
-- Create read-only role for analytics
CREATE ROLE analytics_user WITH LOGIN PASSWORD 'another_password';
GRANT USAGE ON SCHEMA public TO analytics_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_user;
-- Create migration role (for schema changes)
CREATE ROLE migration_user WITH LOGIN PASSWORD 'migration_password';
GRANT ALL ON SCHEMA public TO migration_user;
GRANT ALL ON ALL TABLES IN SCHEMA public TO migration_user;
Connection Strings Per Role
# .env for different environments
# Application (limited permissions)
DATABASE_URL="postgres://app_user:pass@ep-xxx-pooler.../neondb"
# Analytics dashboards (read-only)
ANALYTICS_DATABASE_URL="postgres://analytics_user:pass@ep-xxx-pooler.../neondb"
# Migrations (elevated permissions, short-lived)
MIGRATION_DATABASE_URL="postgres://migration_user:pass@ep-xxx.../neondb"
Branch Security
Neon's branching feature creates instant copies of your database for development and testing.
Safe Branching Practices
# Create a branch for development
neon branches create --name dev --parent main
# Create a branch for a specific feature
neon branches create --name feature-xyz --parent main
# List branches
neon branches list
Data in Branches
Branches contain a copy of all data from the parent branch at the time of creation. If your main branch has production data, be aware that development branches will contain that data too. Consider anonymizing sensitive data or using a separate development database with synthetic data.
Branch-Specific Credentials
# Each branch can have its own credentials
# Generate new password for a branch
neon roles reset-password --branch dev --role app_user
# Use different connection strings per branch
DEV_DATABASE_URL="postgres://app_user:dev_pass@ep-dev-xxx.../neondb"
PROD_DATABASE_URL="postgres://app_user:prod_pass@ep-prod-xxx.../neondb"
Query Security
Even with RLS, you still need to parameterize queries to prevent SQL injection.
Safe Query Patterns
// With Prisma - automatically parameterized
const users = await prisma.user.findMany({
where: { email: { contains: searchInput } }
});
// Raw SQL with Prisma - use tagged template
const results = await prisma.$queryRaw`
SELECT * FROM users
WHERE email ILIKE ${'%' + searchInput + '%'}
`;
// With node-postgres - use parameterized queries
const { rows } = await pool.query(
'SELECT * FROM users WHERE email ILIKE $1',
[`%${searchInput}%`]
);
Serverless Function Configuration
Configure your serverless functions to handle database connections properly.
// Next.js API route with proper connection handling
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export async function GET(request: Request) {
// Set security context
const userId = await getUserId(request);
await sql`SELECT set_config('app.current_user', ${userId}, true)`;
// RLS now enforces access control
const documents = await sql`SELECT * FROM documents`;
return Response.json(documents);
}
// With Drizzle ORM
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
// Queries are automatically parameterized
const users = await db.select().from(users).where(eq(users.id, userId));
Neon Security Checklist
- Connection strings stored in environment variables
- Pooled connections used for serverless functions
- Direct connections used only for migrations
- Row Level Security enabled on all user-data tables
- RLS policies created for all access patterns
- FORCE ROW LEVEL SECURITY enabled on sensitive tables
- Separate database roles created (app, analytics, migration)
- Minimal permissions granted to each role
- Branch credentials rotated and separated
- Production data anonymized in development branches
- All queries use parameterization
- SSL mode set to require in connection strings
IP Allowlisting
Neon supports IP restrictions for additional security:
# In Neon dashboard: Project Settings > IP Allow
# Add specific IPs that should have access:
# - Production server IPs
# - CI/CD runner IPs
# - VPN exit IPs for developers
Audit Logging
Implement audit logging at the database level:
-- Create audit table
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name TEXT NOT NULL,
operation TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID,
timestamp TIMESTAMPTZ DEFAULT NOW()
);
-- Create audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, operation, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END,
current_setting('app.current_user', true)::uuid
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Attach to tables
CREATE TRIGGER audit_users
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
Do I need RLS if I'm already filtering in my application code?
Yes. RLS provides defense in depth. Application bugs, SQL injection, or direct database access could bypass your application filters. RLS enforces access at the database level, making it impossible to accidentally expose data.
::
Why use pooled vs direct connections?
Serverless functions create many short-lived connections. Without pooling, each function invocation opens a new database connection, quickly exhausting Postgres connection limits. The pooler maintains a pool of connections that functions can share.
How do I handle migrations with RLS enabled?
Use a migration role that bypasses RLS, or temporarily disable RLS during migrations. The migration role should have elevated permissions but be used only for schema changes, never in application code.
Is Neon data encrypted?
Yes, Neon encrypts data at rest and in transit. All connections require SSL. For highly sensitive data, consider application-level encryption as an additional layer.
::
What CheckYourVibe Detects
When scanning your Neon-connected project, CheckYourVibe identifies:
- Hardcoded database connection strings
- Missing pooled connections in serverless functions
- Tables without Row Level Security
- Raw SQL queries vulnerable to injection
- Missing SSL in connection strings
- Overly permissive database role usage
Run npx checkyourvibe scan to catch these issues before they reach production.