Skip to content
Back to skills

Database Design

ASecurity

Use when Database design mastery. Schema design with normalization, denormalization strategies, indexing, migration pipelines, ORM selection (Prisma/Drizzle/SQLAlchemy/EF Core), connection pooling, soft deletes, audit trails, multi-tenancy, and serverless database patterns. Use when designing schemas, choosing databases, planning migrations, or architecting data layers.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 27, 2026
ai-agentstypescriptpythongobashsqlexpressrailsdatabasesecurityperformance

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 27, 2026

npx -y skills add Harmitx7/tribunal-kit --skill database-design --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Database Design?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Database Design
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/harmitx7-database-design-tribunal-kit/badge)](https://www.skillsdirectory.com/skills/harmitx7-database-design-tribunal-kit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: database-design
description: "Use when Database design mastery. Schema design with normalization, denormalization strategies, indexing, migration pipelines, ORM selection (Prisma/Drizzle/SQLAlchemy/EF Core), connection pooling, soft deletes, audit trails, multi-tenancy, and serverless database patterns. Use when designing schemas, choosing databases, planning migrations, or architecting data layers."
version: 5.0.0
last-updated: 2026-09-13
skills:
  - supabase-postgres-best-practices
  - sql-pro
  - db-latency-auditor
tools: Read, Grep, Glob, Bash, Edit, Write
scripts-binding:
  - .agent/scripts/lint_runner.js
  - .agent/scripts/verify_all.js
---

# Database Design β€” Schema & Architecture Mastery

---

## πŸ› οΈ Technical Architecture & Reference Recipes

---

## 2026 Database Performance & Schema Invariants

1. **Time-Ordered UUID v7 (RFC 9562)**: When UUIDs are required across distributed systems, use UUID v7 so records append sequentially to B-tree indexes, avoiding fragmentation.
2. **Partial Indexing on Soft Deletes**:
   ```sql
   CREATE INDEX idx_users_active_email ON users (email) WHERE deleted_at IS NULL;
   ```
3. **Covering Indexes**: Use `INCLUDE (col_a, col_b)` to allow index-only scans without table heap lookups on read-heavy query patterns.
4. **Connection Pooling in Serverless**: Always route serverless connections through Supavisor, PgBouncer, or Neon connection poolers with transaction-mode pooling.

## Hallucination Traps (Read First)

- ❌ `TIMESTAMP` without timezone β†’ βœ… Always `TIMESTAMPTZ`
- ❌ UUID v4 as primary key β†’ βœ… UUID v7 (time-ordered) or `BIGINT GENERATED ALWAYS AS IDENTITY`
- ❌ Omitting indexes on foreign keys β†’ βœ… Postgres does NOT auto-index FKs; always create explicit indexes
- ❌ Adding `NOT NULL` column without default directly on large tables β†’ βœ… Add nullable first, backfill in batches, then set `NOT NULL`
- ❌ Soft delete without partial index β†’ βœ… Always index `WHERE deleted_at IS NULL`
- ❌ Direct DB connection inside serverless functions β†’ βœ… Use pooled connection string (PgBouncer/Supavisor)

---

## Database Selection

```
Relational / Complex queries β†’ PostgreSQL (primary choice)
  Serverless PG              β†’ Neon, Supabase
  Edge / Ultra-low latency   β†’ Turso (SQLite @ edge)
  Simple / Embedded          β†’ SQLite
  Global distribution (MySQL) β†’ PlanetScale (no FK support)

Key-value / Cache            β†’ Redis / Valkey / Upstash
Document store               β†’ MongoDB / Firestore
Full-text search             β†’ PostgreSQL tsvector (built-in) or Meilisearch / Typesense
Time-series                  β†’ TimescaleDB / ClickHouse
Vector (AI embeddings)       β†’ pgvector (PostgreSQL ext) / Pinecone / Weaviate
```

---

## Standard Table Template

```sql
CREATE TABLE users (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    -- OR: id UUID DEFAULT gen_random_uuid() PRIMARY KEY (use v7 for perf)
    email       TEXT NOT NULL UNIQUE,
    name        TEXT NOT NULL,
    role        TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'user', 'moderator')),
    is_active   BOOLEAN NOT NULL DEFAULT true,
    metadata    JSONB DEFAULT '{}',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at  TIMESTAMPTZ  -- soft delete
);

-- Required: auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- Required indexes
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_active ON users (email) WHERE deleted_at IS NULL; -- partial index for soft delete
CREATE INDEX idx_users_created_at ON users (created_at DESC);
```

---

## Schema Patterns

### Relationships

```sql
-- One-to-Many: FK on the "many" side + INDEX
CREATE TABLE posts (
    id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    author_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    ...
);
CREATE INDEX idx_posts_author_id ON posts (author_id); -- REQUIRED in Postgres

-- Many-to-Many: junction table with composite PK
CREATE TABLE post_tags (
    post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    tag_id  BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
    PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX idx_post_tags_tag_id ON post_tags (tag_id); -- index the non-PK side
```

### Multi-Tenancy

```sql
-- Pattern 1: tenant_id column (simplest β€” enforce via RLS)
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
    USING (tenant_id = current_setting('app.current_tenant_id')::bigint);

-- Pattern 2: Schema per tenant (better isolation, harder migrations)
-- CREATE SCHEMA tenant_acme;

-- Pattern 3: DB per tenant β€” only for compliance/regulatory needs
```

---

## ORM Selection

| ORM                | Best For                                | Trade-offs                 |
| ------------------ | --------------------------------------- | -------------------------- |
| **Drizzle**        | Edge, TypeScript, bundle-size sensitive | Newer, fewer examples      |
| **Prisma**         | DX, schema management, Prisma Studio    | Heavy, NOT edge-compatible |
| **Kysely**         | Type-safe SQL builder, full control     | Manual migrations          |
| **Raw SQL**        | Complex queries, performance-critical   | Manual type safety         |
| **SQLAlchemy 2.0** | Python async ecosystem                  | Python only                |

```typescript
// Drizzle β€” SQL-like, edge-compatible
const result = await db
  .select({ id: users.id, name: users.name })
  .from(users)
  .where(and(eq(users.role, 'admin'), eq(users.isActive, true)))
  .orderBy(desc(users.createdAt))
  .limit(20);

// Prisma β€” ❌ TRAP: can't express complex joins natively β†’ use prisma.$queryRaw<Type>
const user = await prisma.user.findUnique({ where: { email }, include: { posts: { take: 10 } } });
```

---

## Migrations (Zero-Downtime Strategy)

```sql
-- Safe column add on a large production table:
-- Step 1: Add nullable (no lock)
ALTER TABLE users ADD COLUMN phone TEXT;
-- Step 2: Backfill in batches (non-blocking)
UPDATE users SET phone = '' WHERE phone IS NULL AND id BETWEEN 1 AND 10000;
-- Step 3: Add constraint AFTER all code deploys write the column
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
```

**Migration Rules:**

- Never modify a migration already applied to production β€” create a new one
- Remove column in 2 deploys: first remove all code references, then `DROP COLUMN`
- `CREATE INDEX CONCURRENTLY` to avoid table locks on existing data
- Test migrations against a copy of production data before running live

---

## Indexing Reference

| Index Type         | Use For                                              |
| ------------------ | ---------------------------------------------------- |
| **B-tree**         | General purpose β€” equality & range queries (default) |
| **Hash**           | Equality-only lookups (faster than B-tree for =)     |
| **GIN**            | JSONB, arrays, full-text (`tsvector`)                |
| **GiST**           | Geometric, range types                               |
| **HNSW / IVFFlat** | Vector similarity (pgvector)                         |

**Composite index column order:** equality columns first β†’ range columns last β†’ most selective first

---

## Audit Trail

```sql
CREATE TABLE audit_log (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    table_name TEXT NOT NULL, record_id BIGINT NOT NULL,
    action     TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
    old_data   JSONB, new_data JSONB,
    changed_by BIGINT REFERENCES users(id),
    changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_log_table_record ON audit_log (table_name, record_id);
CREATE INDEX idx_audit_log_changed_at ON audit_log USING brin (changed_at); -- BRIN for time-ordered append-only tables
```

---

## Connection Pooling

```
Without pooling: 100 concurrent requests β†’ 100 DB connections β†’ overwhelms DB
With pooling:    100 concurrent requests β†’ 10–20 reused connections

Sizing formula: max_connections = (cpu_cores Γ— 2) + disk_spindles  (typically 25–50)

Poolers:
  PgBouncer          β†’ External, most common for self-hosted Postgres
  Prisma Accelerate  β†’ Managed, for Prisma projects
  Supabase Supavisor β†’ Managed, for Supabase projects
```

Files in this skill

  • SKILL.md8.4 KB
  • scripts/schema_validator.py5.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…