plugins/ed3d-house-style/skills/howto-develop-with-postgres/SKILL.md
Use when writing database access code, creating schemas, or managing transactions with PostgreSQL - enforces transaction safety with TX_ naming, read-write separation, type safety for UUIDs/JSONB, and snake_case conventions to prevent data corruption and type errors
npx skillsauth add ed3dai/ed3d-plugins-testing howto-develop-with-postgresInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Enforce transaction safety, type safety, and naming conventions to prevent data corruption and runtime errors.
Core principles:
For TypeScript/Drizzle implementations: See typescript-drizzle.md for concrete patterns.
Methods that START transactions:
TX_connection.transaction() or db.transaction() internallyMethods that PARTICIPATE in transactions:
TX_ prefix// GOOD: Starts transaction, has TX_ prefix, no executor parameter
async TX_createUserWithProfile(userData: UserData, profileData: ProfileData): Promise<User> {
return this.db.transaction(async (tx) => {
const user = await this.createUser(userData, tx);
await this.createProfile(user.id, profileData, tx);
return user;
});
}
// GOOD: Participates in transaction, no TX_ prefix, takes executor
async createUser(userData: UserData, executor: Drizzle = this.db): Promise<User> {
return executor.insert(USERS).values(userData).returning();
}
// BAD: Starts transaction but missing TX_ prefix
async createUserWithProfile(userData: UserData, profileData: ProfileData): Promise<User> {
return this.db.transaction(async (tx) => { /* ... */ });
}
// BAD: Has TX_ prefix but takes executor parameter (allows nesting)
async TX_createUser(userData: UserData, executor: Drizzle = this.db): Promise<User> {
return executor.transaction(async (tx) => { /* ... */ });
}
What DOES NOT count as "starting a transaction":
onConflictDoUpdateDefault: ULID stored as UUID
Exceptions (context-dependent):
Rule: If unsure whether data will be user-visible, use ULID.
Use exact decimal types (numeric/decimal) for monetary values:
numeric(19, 4) for general financial dataWhy: Floating-point types accumulate rounding errors. Exact decimal types prevent financial discrepancies.
ALWAYS type JSONB columns in your ORM/schema:
Record<string, unknown> if truly schemalessWhy: Prevents runtime errors from accessing undefined properties or wrong types.
Maintain separate client types at compile time:
Why: Prevents accidental writes to replica, enforces deliberate mutation choices.
All database objects use snake_case:
user_preferences, order_itemscreated_at, user_id, is_activeidx_tablename_columns (e.g., idx_users_email)fk_tablename_reftable (e.g., fk_orders_users)Application code: Map to idiomatic case (camelCase in TypeScript, etc.)
Standard mixins:
created_at, updated_at timestamps on all tablesdeleted_at for soft deletion when neededtenant_id for multi-tenant tables (project-dependent)Proactive indexing:
Default isolation (Read Committed) for most operations.
Use stricter isolation when:
SELECT ... FOR UPDATE)Always use generate + migrate workflow:
Never use auto-push workflow in production.
| Mistake | Reality | Fix | |---------|---------|-----| | "This is one operation, doesn't need transaction" | Multi-step operations without transactions cause partial updates and data corruption | Wrap in transaction with TX_ prefix | | "Single atomic operation needs TX_ prefix" | TX_ is for explicit transaction blocks, not atomic operations | No TX_ for single INSERT/UPDATE/DELETE | | "UUID is just a string" | Type confusion causes runtime errors (wrong ID formats, failed lookups) | Use strict UUID type in language | | "I'll type JSONB later when schema stabilizes" | Untyped JSONB leads to undefined property access and type errors | Type immediately with known fields or Record<string, unknown> | | "Read client vs write client doesn't matter" | Using wrong client bypasses separation, allows accidental mutations | Use read-only client by default, switch deliberately | | "I'll add indexes when we see performance issues" | Missing indexes on foreign keys cause slow queries from day one | Add indexes proactively for FKs and common filters | | "This table won't be user-visible, use serial" | Requirements change, IDs leak in logs/URLs/errors | Use ULID by default unless certain it's internal-only | | "Float/double is fine for money, close enough" | Rounding errors accumulate, causing financial discrepancies (0.01 differences multiply) | Use numeric/decimal types for exact arithmetic |
Transaction management:
.transaction() but no TX_ prefixTX_ prefix but accepts executor parameterType safety:
Schema:
created_at/updated_at timestampsAll of these mean: Stop and fix immediately.
For TypeScript/Drizzle concrete implementations: typescript-drizzle.md
development
Use when the user wants to review a Claude Code session for quality — analyzes the current session (or a specified transcript path) for prompting effectiveness, agent performance, and environment gaps, producing actionable recommendations
development
Use when the user wants to review their recent Claude Code sessions for patterns — analyzes the last N sessions (default 5) in the current project, dispatching parallel reviewers per session, then synthesizing cross-session findings
tools
Use when the user wants to export a Claude Code session transcript as a readable Markdown file — converts the current session (or a specified transcript path) into GitHub-flavored Markdown with metadata header, collapsible tool results, and thinking blocks
development
Use when planning features and need current API docs, library patterns, or external knowledge; when testing hypotheses about technology choices or claims; when verifying assumptions before design decisions - gathers well-sourced, current information from the internet to inform technical decisions