postgres-setup
Set up PostgreSQL database with standardized schema.sql pattern. Use when starting a new project that needs PostgreSQL, setting up database schema, or creating setup scripts for postgres.
When & Why to Use This Skill
This Claude skill automates the professional setup of PostgreSQL databases using a standardized schema.sql pattern and Python initialization scripts. It streamlines the creation of database structures, user permissions, and environment configurations while enforcing industry best practices such as UUID primary keys, Unix timestamps for performance, and connection pooling.
Use Cases
- Bootstrap a new backend project with a production-ready PostgreSQL foundation, including automated directory structure and schema creation.
- Generate idempotent database setup scripts that handle user creation, database provisioning, and permission granting for both local and production environments.
- Implement standardized database schemas that follow best practices like using TEXT over VARCHAR, BIGINT for timestamps, and proper indexing for foreign keys.
- Create isolated test database environments quickly to ensure consistent behavior between development and CI/CD pipelines.
- Develop a robust Python database driver with built-in connection pooling and context managers to ensure safe and efficient resource handling.
| name | postgres-setup |
|---|---|
| description | Set up PostgreSQL database with standardized schema.sql pattern. Use when starting a new project that needs PostgreSQL, setting up database schema, or creating setup scripts for postgres. |
PostgreSQL Database Setup Pattern
This skill helps you set up a PostgreSQL database following a standardized pattern with proper separation of schema and setup scripts.
When to Use This Skill
Use this skill when:
- Starting a new project that needs PostgreSQL
- You want a clean separation between schema definition (SQL) and setup logic (Python)
- You need support for both production and test databases
- You want consistent environment variable patterns
What This Skill Creates
database/schema.sql- SQL schema with table definitionsdev_scripts/setup_database.py- Python setup script- Documentation of required environment variables
Step 1: Gather Project Information
IMPORTANT: Before creating files, ask the user these questions:
"What is your project name?" (e.g., "arcana", "trading-bot", "myapp")
- Use this to derive:
- Database name:
{project_name}(e.g.,arcana) - User name:
{project_name}(e.g.,arcana) - Password env var:
{PROJECT_NAME}_PG_PASSWORD(e.g.,ARCANA_PG_PASSWORD)
- Database name:
- Use this to derive:
"What tables do you need in your schema?" (optional - can create skeleton if unknown)
Step 2: Create Directory Structure
Create these directories if they don't exist:
{project_root}/
├── database/
└── dev_scripts/
Step 3: Create schema.sql
Create database/schema.sql with:
Best Practices to Follow:
- Use
CREATE TABLE IF NOT EXISTSfor idempotency - Use
UUIDfor primary keys withgen_random_uuid()as default - Use
BIGINT(Unix timestamps) for all date/time fields (NOT TIMESTAMP, NOT TIMESTAMPTZ) - Add proper foreign key constraints with
ON DELETE CASCADEorON DELETE SET NULL - Add indexes on foreign keys and commonly queried fields
- Use
TEXTinstead ofVARCHAR(PostgreSQL best practice) - Add comments using
COMMENT ON COLUMNfor documentation
Template Structure:
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Example table
CREATE TABLE IF NOT EXISTS example_table (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at BIGINT NOT NULL DEFAULT extract(epoch from now())::bigint,
updated_at BIGINT
);
-- Add indexes
CREATE INDEX IF NOT EXISTS idx_example_created_at ON example_table(created_at);
-- Add comments
COMMENT ON TABLE example_table IS 'Description of what this table stores';
COMMENT ON COLUMN example_table.created_at IS 'Unix timestamp of creation';
If user provides specific tables, create schema accordingly. Otherwise, create a skeleton with one example table.
Step 4: Create setup_database.py
Create dev_scripts/setup_database.py using this template, substituting project-specific values:
#!/usr/bin/env python
"""
Database setup script for {PROJECT_NAME}
Creates the {project_name} database and user with proper permissions, then applies database/schema.sql
Usage:
python setup_database.py --pg-password <postgres_password>
python setup_database.py --pg-password <postgres_password> --pg-user <superuser>
python setup_database.py --pg-password <postgres_password> --test-db
"""
import os
import sys
import argparse
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
def main():
"""Setup {project_name} database and user"""
parser = argparse.ArgumentParser(description='Setup {PROJECT_NAME} database')
parser.add_argument('--pg-password', required=True,
help='PostgreSQL superuser password (required)')
parser.add_argument('--pg-user', default='postgres',
help='PostgreSQL superuser name (default: postgres)')
parser.add_argument('--test-db', action='store_true',
help='Create {project_name}_test database instead of main {project_name} database')
args = parser.parse_args()
pg_host = os.environ.get('POSTGRES_HOST', 'localhost')
pg_port = os.environ.get('POSTGRES_PORT', '5432')
pg_user = args.pg_user
pg_password = args.pg_password
if args.test_db:
{project_name}_db = '{project_name}_test'
print("Setting up TEST database '{project_name}_test'...")
else:
{project_name}_db = os.environ.get('{PROJECT_NAME}_PG_DB', '{project_name}')
{project_name}_user = os.environ.get('{PROJECT_NAME}_PG_USER', '{project_name}')
{project_name}_password = os.environ.get('{PROJECT_NAME}_PG_PASSWORD', None)
if {project_name}_password is None:
print("Error: {PROJECT_NAME}_PG_PASSWORD environment variable is required")
sys.exit(1)
print(f"Setting up database '{{project_name}_db}' and user '{{project_name}_user}'...")
print(f"Connecting to PostgreSQL at {pg_host}:{pg_port} as {pg_user}")
try:
conn = psycopg2.connect(
host=pg_host,
port=pg_port,
database='postgres',
user=pg_user,
password=pg_password
)
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
cursor.execute("SELECT 1 FROM pg_roles WHERE rolname = %s", ({project_name}_user,))
if not cursor.fetchone():
print(f"Creating user '{{project_name}_user}'...")
cursor.execute(f"CREATE USER {project_name}_user WITH PASSWORD %s", ({project_name}_password,))
print(f"✓ User '{{project_name}_user}' created")
else:
print(f"✓ User '{{project_name}_user}' already exists")
cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s", ({project_name}_db,))
if not cursor.fetchone():
print(f"Creating database '{{project_name}_db}'...")
cursor.execute(f"CREATE DATABASE {{project_name}_db} OWNER {{project_name}_user}")
print(f"✓ Database '{{project_name}_db}' created")
else:
print(f"✓ Database '{{project_name}_db}' already exists")
print("Setting permissions...")
cursor.execute(f"GRANT ALL PRIVILEGES ON DATABASE {{project_name}_db} TO {{project_name}_user}")
print(f"✓ Granted all privileges on database '{{project_name}_db}' to user '{{project_name}_user}'")
conn.close()
print(f"\\nConnecting as '{{project_name}_user}' to apply schema...")
{project_name}_conn = psycopg2.connect(
host=pg_host,
port=pg_port,
database={project_name}_db,
user={project_name}_user,
password={project_name}_password
)
{project_name}_conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
schema_path = os.path.join(repo_root, 'database', 'schema.sql')
if not os.path.exists(schema_path):
print(f"Error: schema file not found at {schema_path}")
sys.exit(1)
with open(schema_path, 'r', encoding='utf-8') as f:
schema_sql = f.read()
with {project_name}_conn.cursor() as cursor:
print("Ensuring required extensions...")
cursor.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
print(f"Applying schema from {schema_path}...")
cursor.execute(schema_sql)
print("✓ Schema applied")
{project_name}_conn.close()
print("✓ Database setup complete")
print(f"Database: {{project_name}_db}")
print(f"User: {{project_name}_user}")
print(f"Host: {pg_host}:{pg_port}")
except psycopg2.Error as e:
print(f"Error: {e}")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
CRITICAL: Replace ALL instances of:
{PROJECT_NAME}→ uppercase project name (e.g.,ARCANA,MYAPP){project_name}→ lowercase project name (e.g.,arcana,myapp)
Step 5: Create Documentation
Add a section to the project's README.md (or create SETUP.md) documenting:
Command Line Arguments
Required:
--pg-password- PostgreSQL superuser password
Optional:
--pg-user- PostgreSQL superuser name (default: postgres)--test-db- Create test database instead of main database
Environment Variables
PostgreSQL connection (optional):
POSTGRES_HOST- PostgreSQL host (default: localhost)POSTGRES_PORT- PostgreSQL port (default: 5432)
Project-specific:
{PROJECT_NAME}_PG_DB- Database name (default: {project_name}){PROJECT_NAME}_PG_USER- Application user (default: {project_name}){PROJECT_NAME}_PG_PASSWORD- Application user password (REQUIRED)
Setup Instructions
# Set required environment variables
export {PROJECT_NAME}_PG_PASSWORD="your_app_password"
# Run setup script (pass postgres superuser password as argument)
python dev_scripts/setup_database.py --pg-password "your_postgres_password"
# With custom superuser name
python dev_scripts/setup_database.py --pg-password "your_postgres_password" --pg-user "admin"
# For test database
python dev_scripts/setup_database.py --pg-password "your_postgres_password" --test-db
Step 6: Make Script Executable
Run:
chmod +x dev_scripts/setup_database.py
Step 7: Create Database Driver (Optional but Recommended)
If the project needs a database driver/connection manager, create one following this pattern:
File: src/{project_name}/driver/database.py
Key patterns to follow:
Connection Pooling: Use
ThreadedConnectionPoolfrom psycopg2from psycopg2.pool import ThreadedConnectionPool self.pool = ThreadedConnectionPool( min_conn, # e.g., 2 max_conn, # e.g., 10 host=db_host, database=db_name, user=db_user, password=db_passwd )Context Managers: Provide context managers for connections and cursors
@contextmanager def get_cursor(self, commit=True, cursor_factory=None): """Context manager for database cursors with automatic commit/rollback""" with self._get_connection() as conn: cursor = conn.cursor(cursor_factory=cursor_factory) try: yield cursor if commit: conn.commit() except Exception: conn.rollback() raise finally: cursor.close()Always Use RealDictCursor for Loading Data: When reading from database, use RealDictCursor
from psycopg2.extras import RealDictCursor with self.get_cursor(commit=False, cursor_factory=RealDictCursor) as cursor: cursor.execute("SELECT * FROM table WHERE id = %s", (id,)) result = cursor.fetchone() return Model.from_dict(dict(result))Unix Timestamps Everywhere: Convert database timestamps to/from unix timestamps
# When saving to DB - store as BIGINT created_at = int(time.time()) # When loading from DB - already BIGINT, use as-is # In models, store as int (unix timestamp) # Only convert to datetime for display/formatting purposesProper Cleanup: Ensure pool is closed on destruction
def close(self): if self.pool and not self.pool.closed: self.pool.closeall() def __del__(self): if hasattr(self, 'pool'): self.close()
Example Driver Structure:
class {ProjectName}DB:
def __init__(self, db_host, db_name, db_user, db_passwd, min_conn=2, max_conn=10):
self.pool = ThreadedConnectionPool(...)
@contextmanager
def get_cursor(self, commit=True, cursor_factory=None):
# Context manager for cursors
pass
def load_item_by_id(self, item_id: str) -> Item:
with self.get_cursor(commit=False, cursor_factory=RealDictCursor) as cursor:
cursor.execute("SELECT * FROM items WHERE id = %s", (item_id,))
result = cursor.fetchone()
if not result:
raise Exception(f"Item {item_id} not found")
return Item.from_dict(dict(result))
def save_item(self, item: Item) -> str:
with self.get_cursor() as cursor:
cursor.execute(
"INSERT INTO items (name, created_at) VALUES (%s, %s) RETURNING id",
(item.name, int(time.time()))
)
return str(cursor.fetchone()[0])
Design Principles
This pattern follows these principles:
Database Schema:
- Separation of concerns - SQL in .sql files, setup logic in Python
- Idempotency - Safe to run multiple times
- Test database support - Easy to create isolated test environments
- Unix timestamps - Always use BIGINT for dates/times (not TIMESTAMP types)
- UUIDs for keys - Better for distributed systems
- Environment-based config - No hardcoded credentials
Database Driver (if applicable):
- Connection pooling - Use ThreadedConnectionPool for efficient connection reuse
- Context managers - Automatic commit/rollback and resource cleanup
- RealDictCursor for reads - Always use RealDictCursor when loading data for easy dict conversion
- Unix timestamps - Store as BIGINT, convert only for display
- Proper cleanup - Close pool on destruction
Example Usage in Claude Code
User: "Set up postgres database for my project" Claude: "What is your project name?" User: "trading-bot" Claude:
- Creates database/ and dev_scripts/ directories
- Creates database/schema.sql with skeleton
- Creates dev_scripts/setup_database.py with:
- TRADING_BOT_PG_PASSWORD
- trading_bot database and user
- Documents environment variables needed
- Makes script executable