Claude Code Plugins

Community-maintained marketplace

Feedback

Design PostgreSQL schemas with migrations, seeding, and documentation. Use when creating tables, writing migrations, or setting up database structure.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name database
description Design PostgreSQL schemas with migrations, seeding, and documentation. Use when creating tables, writing migrations, or setting up database structure.
allowed-tools Read, Write, Edit, Glob, Grep

Database Skill

Design and manage PostgreSQL databases following project conventions for schema design, migrations, seeding, and documentation.


When to Use

  • Creating database tables and schemas
  • Writing migration files
  • Designing indexes and constraints
  • Setting up foreign key relationships
  • Creating seed data for development
  • Documenting database structure

Project Structure

src/db/
├── client.js           # Database connection pool
├── migrations/         # Timestamped migration files
│   ├── 001_create_users.js
│   ├── 002_create_products.js
│   └── 003_add_user_roles.js
├── seeds/              # Development seed data
│   ├── 001_users.js
│   └── 002_products.js
├── queries/            # SQL query files
│   ├── users.js
│   └── products.js
└── schema.sql          # Full schema documentation

Migration File Format

Standard Migration

// src/db/migrations/001_create_users.js

/**
 * Migration: Create users table
 * @param {import('pg').Pool} db - Database pool
 */
export async function up(db) {
  await db.query(`
    CREATE TABLE users (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      email VARCHAR(255) NOT NULL UNIQUE,
      name VARCHAR(255) NOT NULL,
      password_hash VARCHAR(255) NOT NULL,
      role VARCHAR(50) NOT NULL DEFAULT 'user',
      email_verified BOOLEAN NOT NULL DEFAULT FALSE,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );

    CREATE INDEX idx_users_email ON users(email);
    CREATE INDEX idx_users_role ON users(role);

    COMMENT ON TABLE users IS 'Application users';
    COMMENT ON COLUMN users.role IS 'User role: user, admin, moderator';
  `);
}

/**
 * Rollback migration
 * @param {import('pg').Pool} db - Database pool
 */
export async function down(db) {
  await db.query(`DROP TABLE IF EXISTS users CASCADE`);
}

export const description = 'Create users table with auth fields';

Migration with Foreign Keys

// src/db/migrations/002_create_tasks.js

export async function up(db) {
  await db.query(`
    CREATE TABLE tasks (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
      title VARCHAR(255) NOT NULL,
      description TEXT,
      status VARCHAR(50) NOT NULL DEFAULT 'pending',
      priority INTEGER NOT NULL DEFAULT 2,
      due_date DATE,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

      CONSTRAINT chk_status CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
      CONSTRAINT chk_priority CHECK (priority BETWEEN 0 AND 4)
    );

    CREATE INDEX idx_tasks_user_id ON tasks(user_id);
    CREATE INDEX idx_tasks_status ON tasks(status);
    CREATE INDEX idx_tasks_due_date ON tasks(due_date) WHERE due_date IS NOT NULL;

    COMMENT ON TABLE tasks IS 'User tasks and todos';
    COMMENT ON COLUMN tasks.priority IS '0=critical, 1=high, 2=medium, 3=low, 4=backlog';
  `);
}

export async function down(db) {
  await db.query(`DROP TABLE IF EXISTS tasks CASCADE`);
}

export const description = 'Create tasks table with user relationship';

Migration Runner

// src/db/migrate.js
import { db } from './client.js';
import { readdir } from 'fs/promises';
import { join } from 'path';

/**
 * Run pending migrations
 */
export async function migrate() {
  // Ensure migrations table exists
  await db.query(`
    CREATE TABLE IF NOT EXISTS migrations (
      id SERIAL PRIMARY KEY,
      name VARCHAR(255) NOT NULL UNIQUE,
      executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    )
  `);

  // Get executed migrations
  const { rows: executed } = await db.query(
    'SELECT name FROM migrations ORDER BY id'
  );
  const executedNames = new Set(executed.map(r => r.name));

  // Get migration files
  const migrationsDir = new URL('./migrations', import.meta.url).pathname;
  const files = await readdir(migrationsDir);
  const migrations = files
    .filter(f => f.endsWith('.js'))
    .sort();

  // Run pending migrations
  for (const file of migrations) {
    if (executedNames.has(file)) continue;

    console.log(`Running migration: ${file}`);
    const migration = await import(join(migrationsDir, file));

    await db.query('BEGIN');
    try {
      await migration.up(db);
      await db.query(
        'INSERT INTO migrations (name) VALUES ($1)',
        [file]
      );
      await db.query('COMMIT');
      console.log(`  Completed: ${migration.description || file}`);
    } catch (error) {
      await db.query('ROLLBACK');
      console.error(`  Failed: ${error.message}`);
      throw error;
    }
  }

  console.log('All migrations complete');
}

/**
 * Rollback last migration
 */
export async function rollback() {
  const { rows } = await db.query(
    'SELECT name FROM migrations ORDER BY id DESC LIMIT 1'
  );

  if (rows.length === 0) {
    console.log('No migrations to rollback');
    return;
  }

  const file = rows[0].name;
  console.log(`Rolling back: ${file}`);

  const migrationsDir = new URL('./migrations', import.meta.url).pathname;
  const migration = await import(join(migrationsDir, file));

  await db.query('BEGIN');
  try {
    await migration.down(db);
    await db.query('DELETE FROM migrations WHERE name = $1', [file]);
    await db.query('COMMIT');
    console.log('Rollback complete');
  } catch (error) {
    await db.query('ROLLBACK');
    console.error(`Rollback failed: ${error.message}`);
    throw error;
  }
}

Schema Design Patterns

Primary Keys

-- UUID (recommended for distributed systems)
id UUID PRIMARY KEY DEFAULT gen_random_uuid()

-- Serial (simpler, sequential)
id SERIAL PRIMARY KEY

-- Composite key
PRIMARY KEY (user_id, product_id)

Timestamps

-- Standard timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

-- With trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at();

Soft Deletes

-- Soft delete column
deleted_at TIMESTAMPTZ DEFAULT NULL

-- Index for active records
CREATE INDEX idx_users_active ON users(id) WHERE deleted_at IS NULL;

-- Query pattern
SELECT * FROM users WHERE deleted_at IS NULL;

JSON Columns

-- JSONB for structured data
metadata JSONB NOT NULL DEFAULT '{}',
settings JSONB NOT NULL DEFAULT '{}'

-- Index for JSON queries
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);

-- Query JSON
SELECT * FROM users WHERE metadata->>'plan' = 'premium';

Enums vs Check Constraints

-- Check constraint (preferred - easier to modify)
status VARCHAR(50) NOT NULL DEFAULT 'pending',
CONSTRAINT chk_status CHECK (status IN ('pending', 'active', 'completed'))

-- PostgreSQL enum (harder to modify, but type-safe)
CREATE TYPE task_status AS ENUM ('pending', 'active', 'completed');
status task_status NOT NULL DEFAULT 'pending'

Indexes

When to Create Indexes

-- Foreign keys (always index)
CREATE INDEX idx_tasks_user_id ON tasks(user_id);

-- Frequently queried columns
CREATE INDEX idx_users_email ON users(email);

-- Columns used in WHERE clauses
CREATE INDEX idx_tasks_status ON tasks(status);

-- Columns used in ORDER BY
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);

-- Partial indexes (for filtered queries)
CREATE INDEX idx_tasks_pending ON tasks(due_date)
  WHERE status = 'pending';

-- Composite indexes (for multi-column queries)
CREATE INDEX idx_tasks_user_status ON tasks(user_id, status);

Index Types

-- B-tree (default, most common)
CREATE INDEX idx_users_email ON users(email);

-- GIN (for JSONB, arrays, full-text)
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);

-- GiST (for geometric, full-text)
CREATE INDEX idx_locations_coords ON locations USING GIST (coordinates);

-- Hash (for equality only, rarely used)
CREATE INDEX idx_sessions_token ON sessions USING HASH (token);

Seed Data

Seed File Format

// src/db/seeds/001_users.js
import { faker } from '@faker-js/faker';

/**
 * Seed users table
 * @param {import('pg').Pool} db - Database pool
 */
export async function seed(db) {
  // Admin user (consistent for testing)
  await db.query(`
    INSERT INTO users (id, email, name, password_hash, role, email_verified)
    VALUES (
      '00000000-0000-0000-0000-000000000001',
      'admin@example.com',
      'Admin User',
      '$argon2id$v=19$m=65536,t=3,p=4$...',  -- 'password123'
      'admin',
      TRUE
    )
    ON CONFLICT (email) DO NOTHING
  `);

  // Generate fake users
  const users = Array.from({ length: 50 }, () => ({
    email: faker.internet.email(),
    name: faker.person.fullName(),
    password_hash: '$argon2id$v=19$m=65536,t=3,p=4$...',
    role: faker.helpers.arrayElement(['user', 'user', 'user', 'moderator']),
    email_verified: faker.datatype.boolean()
  }));

  for (const user of users) {
    await db.query(`
      INSERT INTO users (email, name, password_hash, role, email_verified)
      VALUES ($1, $2, $3, $4, $5)
      ON CONFLICT (email) DO NOTHING
    `, [user.email, user.name, user.password_hash, user.role, user.email_verified]);
  }

  console.log('Seeded 51 users');
}

export const description = 'Seed users with admin and fake data';

Seed Runner

// src/db/seed.js
import { db } from './client.js';
import { readdir } from 'fs/promises';
import { join } from 'path';

/**
 * Run all seed files
 */
export async function seed() {
  const seedsDir = new URL('./seeds', import.meta.url).pathname;
  const files = await readdir(seedsDir);
  const seeds = files
    .filter(f => f.endsWith('.js'))
    .sort();

  for (const file of seeds) {
    console.log(`Seeding: ${file}`);
    const seedModule = await import(join(seedsDir, file));
    await seedModule.seed(db);
  }

  console.log('All seeds complete');
}

Schema Documentation (schema.sql)

-- schema.sql
-- Generated documentation of database structure
-- Do not run directly - use migrations

-- ============================================
-- USERS
-- ============================================
-- Application users with authentication data

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) NOT NULL UNIQUE,
  name VARCHAR(255) NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  role VARCHAR(50) NOT NULL DEFAULT 'user',  -- user, admin, moderator
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);

-- ============================================
-- TASKS
-- ============================================
-- User tasks with status tracking

CREATE TABLE tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  status VARCHAR(50) NOT NULL DEFAULT 'pending',
  priority INTEGER NOT NULL DEFAULT 2,  -- 0-4
  due_date DATE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Constraints
ALTER TABLE tasks ADD CONSTRAINT chk_status
  CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled'));
ALTER TABLE tasks ADD CONSTRAINT chk_priority
  CHECK (priority BETWEEN 0 AND 4);

-- Indexes
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_due_date ON tasks(due_date) WHERE due_date IS NOT NULL;

Common Queries Pattern

// src/db/queries/users.js

export const userQueries = {
  findById: `
    SELECT id, email, name, role, email_verified, created_at, updated_at
    FROM users
    WHERE id = $1 AND deleted_at IS NULL
  `,

  findByEmail: `
    SELECT id, email, name, role, password_hash, email_verified
    FROM users
    WHERE email = $1 AND deleted_at IS NULL
  `,

  create: `
    INSERT INTO users (email, name, password_hash)
    VALUES ($1, $2, $3)
    RETURNING id, email, name, role, created_at
  `,

  update: `
    UPDATE users
    SET name = COALESCE($2, name),
        email = COALESCE($3, email),
        updated_at = NOW()
    WHERE id = $1 AND deleted_at IS NULL
    RETURNING id, email, name, role, updated_at
  `,

  softDelete: `
    UPDATE users
    SET deleted_at = NOW()
    WHERE id = $1 AND deleted_at IS NULL
    RETURNING id
  `,

  list: `
    SELECT id, email, name, role, created_at
    FROM users
    WHERE deleted_at IS NULL
    ORDER BY created_at DESC
    LIMIT $1 OFFSET $2
  `,

  count: `
    SELECT COUNT(*) as total
    FROM users
    WHERE deleted_at IS NULL
  `
};

Checklist

When designing databases:

  • Use UUID or SERIAL for primary keys consistently
  • Add created_at and updated_at timestamps
  • Index all foreign key columns
  • Index columns used in WHERE and ORDER BY
  • Use check constraints for enums (easier than PostgreSQL ENUMs)
  • Add comments to tables and important columns
  • Write reversible migrations with up/down
  • Include description in migration files
  • Create seeds for admin/test users
  • Use faker.js for realistic seed data
  • Document schema in schema.sql
  • Use TIMESTAMPTZ (not TIMESTAMP) for times
  • Consider soft deletes for important data
  • Use parameterized queries (prevent SQL injection)

Related Skills

  • nodejs-backend - Build Node.js backend services with Express/Fastify, Post...
  • rest-api - Write REST API endpoints with HTTP methods, status codes,...
  • authentication - Implement secure authentication with JWT, sessions, OAuth...
  • security - Write secure web pages and applications