Use when setting up pgdbm database connections, migrations, or multi-service architecture - provides mental model of one pool/many schemas/template syntax and complete API reference without needing to read docs
Provides pgdbm connection patterns and API reference for setting up database connections, migrations, and multi-service architecture. Use when configuring shared pools, schema-isolated managers, or dual-mode libraries to avoid common mistakes.
/plugin marketplace add juanre/pgdbm/plugin install pgdbm@juanre-ai-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Core Principle: One shared connection pool, multiple schema-isolated managers, template-based queries.
pgdbm is designed so code can run standalone (creates own pool) OR embedded (uses shared pool) without changes. This flexibility comes from three key patterns:
{{tables.tablename}} works across all deployment modesNeed database access?
├─ Multiple services/modules in same app?
│ └─ Use SHARED POOL pattern
│ • Create ONE pool with create_shared_pool()
│ • Each service gets AsyncDatabaseManager(pool=shared_pool, schema="service_name")
│ • Each runs own migrations with unique module_name
│
├─ Building reusable library/package?
│ └─ Use DUAL-MODE pattern
│ • Accept EITHER connection_string OR db_manager parameter
│ • Create own pool if standalone, use provided if embedded
│ • ALWAYS run own migrations regardless
│
└─ Simple standalone service?
└─ Use STANDALONE pattern
• AsyncDatabaseManager(DatabaseConfig(...))
• Run migrations with AsyncMigrationManager
# Shared pool (for multi-service apps)
from pgdbm import AsyncDatabaseManager, DatabaseConfig
config = DatabaseConfig(
connection_string="postgresql://user:pass@host/db",
min_connections=5, # Start small, tune based on metrics
max_connections=20, # Keep under your DB's max_connections
)
shared_pool = await AsyncDatabaseManager.create_shared_pool(config)
# Schema-isolated managers (share the pool)
users_db = AsyncDatabaseManager(pool=shared_pool, schema="users")
orders_db = AsyncDatabaseManager(pool=shared_pool, schema="orders")
# Standalone (creates own pool)
db = AsyncDatabaseManager(config)
await db.connect()
from pgdbm import AsyncMigrationManager
migrations = AsyncMigrationManager(
db, # AsyncDatabaseManager instance
migrations_path="./migrations", # Directory with SQL files
module_name="myservice" # REQUIRED: unique identifier
)
result = await migrations.apply_pending_migrations()
# ALWAYS use {{tables.}} syntax
user_id = await db.fetch_value(
"INSERT INTO {{tables.users}} (email) VALUES ($1) RETURNING id",
"user@example.com"
)
# With transactions
async with db.transaction() as tx:
user_id = await tx.fetch_value(
"INSERT INTO {{tables.users}} (email) VALUES ($1) RETURNING id",
email
)
await tx.execute(
"INSERT INTO {{tables.profiles}} (user_id, bio) VALUES ($1, $2)",
user_id,
"Bio text"
)
# Auto-commits on success, rolls back on exception
-- migrations/001_create_users.sql
CREATE TABLE IF NOT EXISTS {{tables.users}} (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS users_email
ON {{tables.users}} (email);
Problem: PostgreSQL has hard connection limits (typically 100-400). Each connection consumes:
With pool-per-service:
With shared pool:
Key insight: Services don't hit peak load simultaneously. Dynamic allocation beats static pre-allocation.
Three options for multi-tenancy/multi-service:
| Approach | Security | Complexity | Use When |
|---|---|---|---|
| Separate DBs | Strongest (PostgreSQL enforced) | High | Regulatory requirements, <1000 tenants |
| Separate Schemas | Good (application enforced) | Medium | 100-10,000 tenants, B2B SaaS |
| Row-level (tenant_id) | Weakest (code must filter) | Low | Millions of tenants, consumer apps |
pgdbm optimizes for schemas because:
Problem: Hardcoding table names locks code to ONE deployment mode.
# ❌ WRONG: Hardcoded - only works in "myapp" schema
await db.execute('INSERT INTO "myapp".users (email) VALUES ($1)', email)
# ✅ CORRECT: Template - works everywhere
await db.execute('INSERT INTO {{tables.users}} (email) VALUES ($1)', email)
How it works:
schema="myapp": {{tables.users}} → "myapp".users{{tables.users}} → usersBenefits:
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
@asynccontextmanager
async def lifespan(app: FastAPI):
# Create ONE shared pool
config = DatabaseConfig(connection_string="postgresql://localhost/myapp")
shared_pool = await AsyncDatabaseManager.create_shared_pool(config)
# Schema-isolated managers
app.state.dbs = {
'users': AsyncDatabaseManager(pool=shared_pool, schema="users"),
'orders': AsyncDatabaseManager(pool=shared_pool, schema="orders"),
}
# Run migrations for each
for name, db in app.state.dbs.items():
migrations = AsyncMigrationManager(
db,
migrations_path=f"migrations/{name}",
module_name=name
)
await migrations.apply_pending_migrations()
yield
await shared_pool.close()
app = FastAPI(lifespan=lifespan)
@app.post("/users")
async def create_user(email: str, request: Request):
db = request.app.state.dbs['users']
user_id = await db.fetch_value(
"INSERT INTO {{tables.users}} (email) VALUES ($1) RETURNING id",
email
)
return {"id": user_id}
from typing import Optional
from pgdbm import AsyncDatabaseManager, DatabaseConfig
class MyLibrary:
"""Library that works standalone OR embedded."""
def __init__(
self,
connection_string: Optional[str] = None,
db_manager: Optional[AsyncDatabaseManager] = None,
):
if not connection_string and not db_manager:
raise ValueError("Provide connection_string OR db_manager")
self._external_db = db_manager is not None
self.db = db_manager
self._connection_string = connection_string
async def initialize(self):
# Create pool only if not provided
if not self._external_db:
config = DatabaseConfig(connection_string=self._connection_string)
self.db = AsyncDatabaseManager(config)
await self.db.connect()
# ALWAYS run own migrations
migrations = AsyncMigrationManager(
self.db,
migrations_path="./migrations",
module_name="mylib" # Unique!
)
await migrations.apply_pending_migrations()
async def close(self):
# Only close if WE created the connection
if self.db and not self._external_db:
await self.db.disconnect()
If you're about to do any of these, you're missing the mental model:
AsyncDatabaseManager instances with DatabaseConfig to same databaseINSERT INTO "myschema".users{{tables.}} syntax in queries or migrationsschema parameter to AsyncMigrationManager (it reads from db)module_name in migrations (causes conflicts)db.schema at runtime (create separate managers instead)INSERT INTO users (not schema-safe)All of these mean: Review the mental model. You're fighting the library instead of using its design.
For COMPLETE AsyncDatabaseManager API: See pgdbm:core-api-reference skill
Includes ALL methods:
execute, executemany, execute_and_return_idfetch_one, fetch_all, fetch_valuefetch_as_model, fetch_all_as_model (Pydantic)copy_records_to_table (bulk operations)table_exists, get_pool_statstransaction, acquireFor COMPLETE AsyncMigrationManager API: See pgdbm:migrations-api-reference skill
Includes ALL methods:
apply_pending_migrations, get_applied_migrationsget_pending_migrations, find_migration_filescreate_migration, rollback_migrationget_migration_historyQuick reference for most common operations:
# Query methods
count = await db.fetch_value("SELECT COUNT(*) FROM {{tables.users}}")
user = await db.fetch_one("SELECT * FROM {{tables.users}} WHERE id = $1", user_id)
users = await db.fetch_all("SELECT * FROM {{tables.users}}")
await db.execute("DELETE FROM {{tables.users}} WHERE id = $1", user_id)
user_id = await db.execute_and_return_id("INSERT INTO {{tables.users}} ...", ...)
# Transactions
async with db.transaction() as tx:
await tx.execute(...)
await tx.fetch_value(...)
# Migrations
migrations = AsyncMigrationManager(db, "migrations", module_name="myapp")
result = await migrations.apply_pending_migrations()
Available in all SQL queries and migration files:
{{tables.tablename}} -- Expands to: "schema".tablename (or just tablename if no schema)
{{schema}} -- Expands to: "schema" (or empty if no schema)
How expansion works:
# With schema="myapp"
"SELECT * FROM {{tables.users}}" → "SELECT * FROM \"myapp\".users"
# Without schema
"SELECT * FROM {{tables.users}}" → "SELECT * FROM users"
# app.py - Complete production setup
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pgdbm import AsyncDatabaseManager, DatabaseConfig, AsyncMigrationManager
@asynccontextmanager
async def lifespan(app: FastAPI):
# ONE shared pool
config = DatabaseConfig(
connection_string="postgresql://localhost/myapp",
min_connections=5,
max_connections=20,
)
shared_pool = await AsyncDatabaseManager.create_shared_pool(config)
# Schema-isolated managers
app.state.users_db = AsyncDatabaseManager(pool=shared_pool, schema="users")
app.state.orders_db = AsyncDatabaseManager(pool=shared_pool, schema="orders")
# Run migrations
for db, path, name in [
(app.state.users_db, "migrations/users", "users"),
(app.state.orders_db, "migrations/orders", "orders"),
]:
migrations = AsyncMigrationManager(db, path, name)
await migrations.apply_pending_migrations()
yield
await shared_pool.close()
app = FastAPI(lifespan=lifespan)
| Scenario | Pattern | Key Points |
|---|---|---|
| FastAPI app with 3 services | Shared pool | ONE create_shared_pool(), each service gets schema |
| Building PyPI package | Dual-mode library | Accept db_manager OR connection_string |
| Simple microservice | Standalone | AsyncDatabaseManager(config) |
| Multi-tenant SaaS | Shared pool + schemas | Each tenant = separate schema |
| Testing | Use fixtures | Import from pgdbm.fixtures.conftest |
For pattern selection:
pgdbm:choosing-pattern - Which pattern for your use caseFor implementation:
pgdbm:shared-pool-pattern - Multi-service appspgdbm:dual-mode-library - PyPI packagespgdbm:standalone-service - Simple servicesFor complete API:
pgdbm:core-api-reference - ALL AsyncDatabaseManager methodspgdbm:migrations-api-reference - ALL AsyncMigrationManager methodsFor testing and quality:
pgdbm:testing-database-code - Test fixturespgdbm:common-mistakes - Anti-patterns