database-schema-design — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited database-schema-design (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ask:
Apply normalization rules:
First Normal Form (1NF):
Second Normal Form (2NF):
Third Normal Form (3NF):
When to denormalize:
Always start normalized, then denormalize with documented justification.
Use this format for each table:
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
currency CHAR(3) NOT NULL DEFAULT 'USD',
shipping_address_id UUID REFERENCES addresses(id),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE orders IS 'Customer purchase orders';
COMMENT ON COLUMN orders.total_cents IS 'Order total in smallest currency unit';Conventions:
One-to-Many (e.g., user has many orders):
-- FK on the "many" side
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id);Many-to-Many (e.g., orders have many products):
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price_cents BIGINT NOT NULL,
UNIQUE (order_id, product_id)
);One-to-One (e.g., user has one profile):
CREATE TABLE user_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
bio TEXT,
avatar_url TEXT
);Create indexes based on query patterns:
-- Columns used in WHERE clauses
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status) WHERE status != 'cancelled';
-- Columns used in ORDER BY
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
-- Composite index for common filter + sort
CREATE INDEX idx_orders_user_status_date
ON orders(user_id, status, created_at DESC);
-- Full-text search
CREATE INDEX idx_products_search
ON products USING GIN(to_tsvector('english', name || ' ' || description));
-- Unique constraints that are also indexes
CREATE UNIQUE INDEX idx_users_email ON users(LOWER(email));Index rules:
pg_stat_user_indexes to find unused indexesRaw SQL (Flyway-compatible naming: V001__create_users.sql):
-- V001__create_users.sql
BEGIN;
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_users_email ON users(LOWER(email));
COMMIT;Prisma schema:
model User {
id String @id @default(uuid())
email String @unique
orders Order[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}Alembic (Python):
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.UUID(), primary_key=True, server_default=sa.text('gen_random_uuid()')),
sa.Column('email', sa.String(255), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index('idx_users_email', 'users', [sa.text('LOWER(email)')], unique=True)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();Apply this trigger to every table with an updated_at column.
User says: "Design a database for a blog with users, posts, and comments"
Response:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.