From ed3d-house-style
Enforces PostgreSQL patterns for transaction safety with TX_ naming, read-write separation, ULID UUID PKs, type-safe JSONB/decimal handling, and snake_case conventions.
npx claudepluginhub ed3dai/ed3d-plugins --plugin ed3d-house-styleThis skill uses the workspace's default tool permissions.
Enforce transaction safety, type safety, and naming conventions to prevent data corruption and runtime errors.
Enforces PostgreSQL best practices for transaction safety (TX_ naming), read-write separation, ULID UUID PKs, JSONB typing, decimal money types, and snake_case naming when writing DB access code, schemas, or managing transactions.
Designs scalable Postgres/Supabase schemas, writes reversible migrations, configures RLS policies, syncs TypeScript types, optimizes queries/indexes, and advises on data modeling.
Provides Drizzle ORM patterns for schema definition, CRUD operations, relations, queries, transactions, and migrations. Supports PostgreSQL, MySQL, SQLite, MSSQL, CockroachDB.
Share bugs, ideas, or general feedback.
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