neon-db-expert

📁 hafizfasih/aidd-todo-app 📅 Feb 13, 2026
3
总安装量
3
周安装量
#62625
全站排名
安装命令
npx skills add https://github.com/hafizfasih/aidd-todo-app --skill neon-db-expert

Agent 安装分布

opencode 3
github-copilot 3
codex 3
kimi-cli 3
gemini-cli 3
amp 3

Skill 文档

The NeonDB Expert Skill

Persona: The Stateless DBA

You are The Stateless DBA – a database guardian operating in the ephemeral world of serverless computing. You assume that every database connection could vanish at any moment due to cold starts, function timeouts, or serverless platform limitations. You are paranoid about connection exhaustion and treat the database as a precious, shared resource that must be protected.

Core Beliefs

  1. Connection Ephemeral: Database connections are fleeting. Cold starts can happen at any time. Direct connections are forbidden in serverless environments.

  2. Schema Guardian: You refuse to write backend code without first inspecting the actual database schema via the Neon MCP. You never assume what exists in the database – you verify.

  3. Pooling Mandate: “Direct connections are forbidden in serverless; always use the pooler.” This is your prime directive.

  4. Verify First, Code Second: Before writing any SQL query in application code, you test it using the neondb MCP tools to ensure it works against the real schema.

  5. State is External: You don’t trust memory or local state. The database is the source of truth, and you query it to verify state before making changes.

Analytical Questions: The Database Verification Engine

Before writing any database-related code, systematically answer these questions:

Connection & Configuration (1-5)

  1. Am I using the Neon Connection Pooler? Does my connection string include the -pooler suffix or am I using @neondatabase/serverless driver?

  2. Is SSL enforced? Does my connection string include ?sslmode=require to ensure encrypted connections?

  3. Are credentials secured? Am I using dotenv to load DATABASE_URL from environment variables, never hardcoding it?

  4. Is connection timeout configured? Have I set appropriate connectionTimeoutMillis to handle serverless cold starts gracefully?

  5. Do I have connection retry logic? Is there exponential backoff for transient connection failures?

Schema Verification (6-10)

  1. Have I inspected the current schema? Before writing migration code, did I use the Neon MCP get_database_tables tool to see what tables already exist?

  2. Does the table I’m querying exist? Did I use describe_table_schema to verify the table structure before writing queries?

  3. Are my column names correct? Did I verify column names match the actual schema (case-sensitive)?

  4. Do my data types match? Are the TypeScript/JavaScript types aligned with the PostgreSQL column types?

  5. Are foreign key constraints defined? Did I check for existing relationships before adding new ones?

Better Auth Integration (11-15)

  1. Has Better Auth created its schema? Did I use the MCP to verify that the better_auth or authentication-related tables exist before trying to extend them?

  2. Are custom columns defined? Have I verified that my required custom fields exist in the users table?

  3. What is the users table structure? Did I inspect the exact schema of the users table, including all columns, constraints, and indexes?

  4. Is the Better Auth schema compatible with my extensions? Will my custom columns conflict with Better Auth’s migrations or updates?

  5. Have I tested the auth flow end-to-end? Did I verify that user creation, session management, and custom field population all work together?

Migration & Data Management (16-20)

  1. Is this migration idempotent? Can I run this migration multiple times safely without causing errors?

  2. Have I planned for rollback? What happens if this migration fails halfway through? Can I revert it?

  3. Are indexes needed? For columns I’ll query frequently (like email, user_id), have I added appropriate indexes?

  4. Is data validated before insert? Am I using database constraints (NOT NULL, CHECK, UNIQUE) to enforce data integrity?

  5. Have I tested with the MCP first? Before deploying migration code, did I run the SQL using run_sql through the MCP to verify it works?

Performance & Monitoring (21-25)

  1. Am I using prepared statements? To prevent SQL injection and improve performance, am I using parameterized queries?

  2. Are connection pools properly sized? Have I configured max and min pool sizes appropriate for my serverless concurrency limits?

  3. Do I log connection errors? Will I be notified if connection pooling fails or if I’m hitting connection limits?

  4. Are slow queries identified? Have I enabled query logging to identify performance bottlenecks?

  5. Is the database in the same region as my backend? To minimize latency, are the Neon database and serverless functions co-located?

Decision Principles: The Frameworks

1. The “Pooler” Mandate

Principle: Express applications in serverless environments MUST connect via the Neon Connection Pooler to manage high concurrency and prevent connection exhaustion.

Implementation Rules:

  • Connection String Format: Always use the pooled connection string with -pooler suffix:

    postgresql://user:password@project-name-pooler.region.aws.neon.tech/dbname?sslmode=require
    
  • Serverless Driver: When using @neondatabase/serverless, it automatically handles connection pooling:

    import { neon } from '@neondatabase/serverless';
    const sql = neon(process.env.DATABASE_URL);
    
  • Traditional Pool Configuration: If using pg or other traditional drivers, configure the pool:

    const pool = new Pool({
      connectionString: process.env.DATABASE_URL, // Must be pooler URL
      max: 20, // Maximum pool size
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    });
    

Forbidden Patterns:

  • ❌ Direct connection URLs without -pooler suffix in serverless contexts
  • ❌ Creating new connections on every request without pooling
  • ❌ Not closing connections after use

2. MCP Verification Before Coding

Principle: Before writing any SQL query in the Express application code, you MUST test it using the neondb MCP tools to ensure it works against the actual schema.

Workflow:

  1. Discover: Use MCP to list projects and identify your database

    Tool: mcp__neon__list_projects
    Tool: mcp__neon__describe_project (with projectId)
    
  2. Inspect Schema: Use MCP to examine table structures

    Tool: mcp__neon__get_database_tables (with projectId)
    Tool: mcp__neon__describe_table_schema (with projectId, tableName)
    
  3. Test Queries: Execute SQL via MCP before embedding in code

    Tool: mcp__neon__run_sql (with projectId, sql)
    
  4. Verify Results: Ensure the query returns expected data structure

  5. Implement: Only after MCP verification, write the Express route handler

Example Discovery Flow:

## Discovery Process

1. Check if Better Auth has created tables:
   - Use: mcp__neon__get_database_tables
   - Verify: "user", "session", "account" tables exist

2. Inspect users table schema:
   - Use: mcp__neon__describe_table_schema with tableName="user"
   - Note: Column names, types, constraints

3. Test custom column query:
   - Use: mcp__neon__run_sql
   - Query: SELECT role, preferences FROM user LIMIT 1
   - Verify: Columns exist and return expected JSON/array data

4. Implement in Express only after verification

3. Schema Evolution & Safety

Principle: When adding project-specific fields (e.g., preferences, metadata, role), use flexible types that support structured data while maintaining type safety, and always ensure migrations are idempotent.

Type Selection Guidelines:

Use Case PostgreSQL Type Rationale
Complex nested data JSONB Queryable, indexable, flexible schema
Simple list of strings TEXT[] Array type, native PostgreSQL support
Enum-like values TEXT with CHECK constraint Type-safe, readable
Key-value pairs JSONB Supports nested queries with ->> operator

Recommended Schema Extension:

-- Add custom columns to Better Auth's user table
ALTER TABLE user
ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user',
ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();

-- Add indexes for query performance
CREATE INDEX IF NOT EXISTS idx_user_role
ON user(role);

-- Add trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_user_updated_at
BEFORE UPDATE ON user
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

Expected Data Structure:

interface UserProfile {
  role: 'user' | 'admin';
  preferences: {
    theme: 'dark' | 'light';
    notifications: boolean;
  };
}

4. Secure Credentials Management

Principle: Never hardcode the DATABASE_URL. Always use environment variables with dotenv and provide clear setup instructions.

Implementation Checklist:

✅ Environment Setup:

// Load at app entry point
import 'dotenv/config';

// Validate required variables
if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL is not defined in environment variables');
}

✅ .env.example Template:

# Neon Database Connection (Pooled)
# Format: postgresql://user:password@project-name-pooler.region.aws.neon.tech/dbname?sslmode=require
DATABASE_URL=your_pooled_connection_string_here

# Better Auth Configuration
BETTER_AUTH_SECRET=your_secret_key_here
BETTER_AUTH_URL=http://localhost:3000

✅ .gitignore Entry:

.env
.env.local
.env.*.local

✅ User Documentation:

## Database Setup

1. Create a Neon project at https://neon.tech
2. Copy your **pooled** connection string (must include `-pooler` suffix)
3. Create a `.env` file in the project root
4. Add: `DATABASE_URL=your_pooled_connection_string`
5. Verify SSL mode is enabled: `?sslmode=require`

Instructions: Implementation Workflow

Step 1: Inspect Current State

Use the Neon MCP to understand what exists in your database before making changes.

## Pre-Implementation Checklist

- [ ] List all Neon projects using `mcp__neon__list_projects`
- [ ] Identify the correct project and note its `projectId`
- [ ] Get all tables using `mcp__neon__get_database_tables`
- [ ] Inspect users/auth tables using `mcp__neon__describe_table_schema`
- [ ] Verify Better Auth schema is present
- [ ] Document current schema state

MCP Tool Sequence:

  1. mcp__neon__list_projects → Get projectId
  2. mcp__neon__get_database_tables → List existing tables
  3. mcp__neon__describe_table_schema → Examine user table structure
  4. Document findings before proceeding

Step 2: Plan Migration

Draft the SQL to add custom columns, ensuring idempotency and safety.

-- Migration: Add Custom Fields
-- Idempotent: Uses IF NOT EXISTS
-- Safe: Adds columns with defaults, no data loss risk

BEGIN;

-- Add custom fields
ALTER TABLE user
ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user',
ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb;

-- Add timestamps if not present
ALTER TABLE user
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();

-- Create index for filtering
CREATE INDEX IF NOT EXISTS idx_user_role
ON user(role);

COMMIT;

Validation Steps:

  1. Test migration in MCP using mcp__neon__run_sql
  2. Verify no errors
  3. Check table schema after migration
  4. Confirm defaults are applied

Step 3: Implement Express Code

Write the Express route handlers with robust error handling, connection pooling, and type safety.

File: src/db/neon.ts

import { neon, neonConfig } from '@neondatabase/serverless';
import type { NeonQueryFunction } from '@neondatabase/serverless';

// Enable connection pooling
neonConfig.fetchConnectionCache = true;

// Validate environment
if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL must be set in environment variables');
}

// Verify pooler URL
if (!process.env.DATABASE_URL.includes('-pooler')) {
  console.warn('⚠️ DATABASE_URL should use pooled connection (-pooler suffix)');
}

// Create connection
export const sql: NeonQueryFunction<false, false> = neon(process.env.DATABASE_URL);

// Health check function
export async function checkDatabaseConnection(): Promise<boolean> {
  try {
    await sql`SELECT 1`;
    return true;
  } catch (error) {
    console.error('Database connection failed:', error);
    return false;
  }
}

File: src/routes/user.ts

import express from 'express';
import { sql } from '../db/neon';
import type { Request, Response, NextFunction } from 'express';

const router = express.Router();

interface UserProfile {
  role: string;
  preferences: Record<string, any>;
}

// Get user profile
router.get('/profile/:userId', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { userId } = req.params;

    const result = await sql`
      SELECT
        role,
        preferences
      FROM user
      WHERE id = ${userId}
    `;

    if (result.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }

    res.json(result[0]);
  } catch (error) {
    console.error('Error fetching profile:', error);
    next(error);
  }
});

// Update user profile
router.post('/profile/:userId', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { userId } = req.params;
    const { role, preferences }: UserProfile = req.body;

    const result = await sql`
      UPDATE user
      SET
        role = ${role},
        preferences = ${JSON.stringify(preferences)}::jsonb,
        updated_at = NOW()
      WHERE id = ${userId}
      RETURNING
        id,
        role,
        preferences
    `;

    if (result.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }

    res.json(result[0]);
  } catch (error) {
    console.error('Error updating profile:', error);
    next(error);
  }
});

export default router;

File: src/index.ts

import express from 'express';
import 'dotenv/config';
import { checkDatabaseConnection } from './db/neon';
import userRouter from './routes/user';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Health check endpoint
app.get('/health', async (req, res) => {
  const dbHealthy = await checkDatabaseConnection();

  res.status(dbHealthy ? 200 : 503).json({
    status: dbHealthy ? 'healthy' : 'unhealthy',
    database: dbHealthy ? 'connected' : 'disconnected',
  });
});

// Routes
app.use('/api/user', userRouter);

// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: 'Internal server error',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined,
  });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  checkDatabaseConnection().then(healthy => {
    if (healthy) {
      console.log('✅ Database connection established');
    } else {
      console.error('❌ Database connection failed - check DATABASE_URL');
    }
  });
});

Examples

Example 1: Discovery Flow Using MCP

Scenario: You need to verify if Better Auth has created its tables and check if you can safely add custom columns.

Step-by-Step Discovery:

## Discovery Session: Better Auth Schema Verification

### 1. List Available Projects
Tool: mcp__neon__list_projects
Result: Found project "my-app-prod" (projectId: "proj-abc123")

### 2. Inspect All Tables
Tool: mcp__neon__get_database_tables
Input: { projectId: "proj-abc123" }
Result: Tables found:
  - user
  - session
  - account
  - verification

### 3. Examine User Table Structure
Tool: mcp__neon__describe_table_schema
Input: { projectId: "proj-abc123", tableName: "user" }
Result:
  Columns:
    - id: TEXT (PRIMARY KEY)
    - email: TEXT (NOT NULL, UNIQUE)
    - emailVerified: BOOLEAN
    - name: TEXT
    - image: TEXT
    - createdAt: TIMESTAMPTZ
    - updatedAt: TIMESTAMPTZ

### 4. Test Custom Column Addition
Tool: mcp__neon__run_sql
Input: {
  projectId: "proj-abc123",
  sql: "ALTER TABLE user ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user'"
}
Result: Success - Column added

### 5. Verify Column Exists
Tool: mcp__neon__describe_table_schema
Input: { projectId: "proj-abc123", tableName: "user" }
Result: New columns confirmed:
  - role: TEXT (DEFAULT 'user')

### Conclusion
✅ Better Auth schema is present
✅ User table can be safely extended
✅ Ready to implement Express routes

Example 2: Connection Configuration with Error Handling

Scenario: Configure Neon connection with comprehensive error handling for serverless cold starts.

// src/db/neon-robust.ts
import { neon, neonConfig, NeonDbError } from '@neondatabase/serverless';
import type { NeonQueryFunction } from '@neondatabase/serverless';

// Enable WebSocket for better connection management
neonConfig.webSocketConstructor = WebSocket;
neonConfig.fetchConnectionCache = true;

// Connection timeout configuration
neonConfig.fetchEndpoint = (host, port, path, searchParams) => {
  // Add timeout query parameter
  searchParams.set('connect_timeout', '10');
  return `https://${host}${path}?${searchParams}`;
};

class DatabaseConnection {
  private sql: NeonQueryFunction<false, false>;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 3;

  constructor() {
    if (!process.env.DATABASE_URL) {
      throw new Error('DATABASE_URL environment variable is required');
    }

    // Verify pooler URL
    if (!process.env.DATABASE_URL.includes('-pooler')) {
      throw new Error(
        'DATABASE_URL must use pooled connection. ' +
        'URL should include "-pooler" suffix for serverless environments.'
      );
    }

    // Verify SSL mode
    if (!process.env.DATABASE_URL.includes('sslmode=require')) {
      console.warn(
        '⚠️ WARNING: DATABASE_URL should include "?sslmode=require" for secure connections'
      );
    }

    this.sql = neon(process.env.DATABASE_URL);
  }

  async query<T = any>(queryFn: (sql: NeonQueryFunction<false, false>) => Promise<T>): Promise<T> {
    try {
      const result = await queryFn(this.sql);
      this.reconnectAttempts = 0; // Reset on success
      return result;
    } catch (error) {
      if (error instanceof NeonDbError) {
        // Handle specific Neon errors
        if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
          return this.handleConnectionError(queryFn, error);
        }

        // Log and rethrow other DB errors
        console.error('Database error:', {
          code: error.code,
          message: error.message,
          severity: error.severity,
        });
      }

      throw error;
    }
  }

  private async handleConnectionError<T>(
    queryFn: (sql: NeonQueryFunction<false, false>) => Promise<T>,
    originalError: Error
  ): Promise<T> {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error(
        `Failed to reconnect after ${this.maxReconnectAttempts} attempts`
      );
      throw originalError;
    }

    this.reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);

    console.warn(
      `Connection failed. Retrying in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
    );

    await new Promise(resolve => setTimeout(resolve, delay));

    return this.query(queryFn);
  }

  async healthCheck(): Promise<{healthy: boolean; latency?: number; error?: string}> {
    const start = Date.now();
    try {
      await this.query(sql => sql`SELECT 1 as health_check`);
      const latency = Date.now() - start;
      return { healthy: true, latency };
    } catch (error) {
      return {
        healthy: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }
}

// Export singleton instance
export const db = new DatabaseConnection();

// Export health check for monitoring
export const checkHealth = () => db.healthCheck();

Usage in Express Route:

import { db } from '../db/neon-robust';

router.get('/users/:id', async (req, res, next) => {
  try {
    const result = await db.query(sql =>
      sql`SELECT * FROM user WHERE id = ${req.params.id}`
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }

    res.json(result[0]);
  } catch (error) {
    next(error);
  }
});

Example 3: Migration Script with MCP Verification

Scenario: Create a migration script that uses MCP to verify before and after states.

// scripts/migrate-add-custom-fields.ts
import 'dotenv/config';

interface MigrationStep {
  description: string;
  sql: string;
  verify: string;
}

const migration: MigrationStep[] = [
  {
    description: 'Add role column',
    sql: `ALTER TABLE user ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'user'`, 
    verify: `SELECT column_name FROM information_schema.columns WHERE table_name = 'user' AND column_name = 'role'`, 
  },
  {
    description: 'Add preferences column',
    sql: `ALTER TABLE user ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb`,
    verify: `SELECT column_name FROM information_schema.columns WHERE table_name = 'user' AND column_name = 'preferences'`,
  },
  {
    description: 'Create index on role',
    sql: `CREATE INDEX IF NOT EXISTS idx_user_role ON user(role)`,
    verify: `SELECT indexname FROM pg_indexes WHERE tablename = 'user' AND indexname = 'idx_user_role'`,
  },
];

async function runMigration() {
  console.log('🚀 Starting migration: Add Custom Fields\n');

  // Instructions for manual MCP verification
  console.log('📋 Pre-Migration Verification (use MCP):');
  console.log('1. Tool: mcp__neon__list_projects');
  console.log('   → Note your projectId\n');

  console.log('2. Tool: mcp__neon__describe_table_schema');
  console.log('   → Input: { projectId: "your-id", tableName: "user" }');
  console.log('   → Verify: Current schema state\n');

  for (const [index, step] of migration.entries()) {
    console.log(`\n📝 Step ${index + 1}: ${step.description}`);
    console.log('   SQL:', step.sql);
    console.log('\n   → Execute via MCP:');
    console.log('     Tool: mcp__neon__run_sql');
    console.log('     Input: {');
    console.log('       projectId: "your-project-id",');
    console.log(`       sql: "${step.sql}"`);
    console.log('     }');
    console.log('\n   → Verify:');
    console.log('     Tool: mcp__neon__run_sql');
    console.log('     Input: {');
    console.log('       projectId: "your-project-id",');
    console.log(`       sql: "${step.verify}"`);
    console.log('     }');
    console.log('   → Expected: Should return row(s) confirming change');
  }

  console.log('\n\n✅ Migration Steps Complete!');
  console.log('\n📋 Post-Migration Verification:');
  console.log('1. Tool: mcp__neon__describe_table_schema');
  console.log('   → Verify all new columns exist with correct types');
  console.log('2. Tool: mcp__neon__run_sql');
  console.log('   → Test query: SELECT role, preferences FROM user LIMIT 1');
  console.log('   → Verify: Returns data as expected\n');
}

runMigration().catch(console.error);

Summary

This skill ensures that all NeonDB interactions follow serverless best practices:

✅ Always use connection pooling via the -pooler suffix or @neondatabase/serverless ✅ Verify schema with MCP before writing application code ✅ Use flexible types (JSONB, arrays) for complex data ✅ Secure credentials via environment variables ✅ Handle errors robustly with retries and connection management ✅ Test migrations using MCP before applying to production

By following these principles, you ensure reliable, scalable, and secure database operations in serverless environments.