skills/db-migration/SKILL.md
Use this skill whenever the user needs to create a database migration — new table, add column, add index, backfill data, rename column, drop a column, or change a constraint — for PostgreSQL projects using Liquibase or Flyway. ALWAYS trigger on: "migration", "new table", "schema change", "add column", "alter table", "add index", "create index", "drop column", "rename column", "backfill", "liquibase", "flyway", "db changeset", "schema evolution", "changelog". Implicit triggers: user describes a new entity that needs persistence, user adds a field to an existing entity (needs matching migration), user talks about a one-time data fix, user asks about zero-downtime migrations, user mentions PostgreSQL `CONCURRENTLY`, user wants to add a foreign key, user asks to normalize or denormalize a table. Auto-detects mode: **Liquibase** (Kifiya monorepo, `db/changelog/changes/sql/*.sql` with `--liquibase formatted sql` header) or **Flyway 10+** (new projects, `V{yyyyMMddHHmmss}__{snake_case}.sql`). Encodes user conventions: naming, audit columns (`created_by`, `created_at TIMESTAMPTZ DEFAULT now()`, `last_modified_by`, `updated_at`), BigInt PKs with application-generated TSIDs, `NUMERIC(20,2)` for fiat money, `NUMERIC(20,8)` for crypto, explicit rollback on every changeset, safety rules (never DROP without confirmation, always CONCURRENTLY for index on populated tables, always ON CONFLICT DO NOTHING for seeds). Produces the migration file, a matching verification query, and a rollback test.
npx skillsauth add OmexIT/claude-skills-pack db-migrationInstall 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.
Generates safe, reversible PostgreSQL migrations for Liquibase or Flyway with naming, audit columns, rollback, and production-safety checks.
This skill generates SQL migrations. Migrations have no classical TDD but they have strict verification requirements.
superpowers:test-driven-development for the SQL class.db-migration) to generate the migration changeset. Sequential execution only — migrations have strict ordering; never parallel.CONCURRENTLY on populated tables), audit column discipline.Destructive operation gate: this skill must never generate DROP TABLE, DROP COLUMN, column renames, or type changes without explicit user confirmation shown as a prompt. If the user tries to bypass the prompt, the skill refuses.
# Liquibase indicators
find src/main/resources/db/changelog -name "*.sql" 2>/dev/null | head -3
grep -l "liquibase formatted sql" src/main/resources/db/changelog/**/*.sql 2>/dev/null | head -3
# Flyway indicators
find src/main/resources/db/migration -name "V*__*.sql" 2>/dev/null | head -3
grep -rE "flyway-core|spring-boot-starter-flyway" build.gradle* pom.xml 2>/dev/null
# Report mode:
🗄️ MIGRATION MODE DETECTED
Project: <name>
Mode: <liquibase | flyway>
Location: <path to changelog/migration dir>
Proceeding with <mode>-specific format.
If neither detected → ask which to use, default to Liquibase for projects that have a PayserFlow-like db/changelog/changes/sql/ structure.
id BIGINT PRIMARY KEY -- application-generated TSID (time-sorted, 64-bit)
Never SERIAL. Never UUID. Never IDENTITY. IDs are generated by Hypersistence TSID in the application, passed as a BIGINT.
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_modified_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
amount_fiat NUMERIC(20,2) -- USD, EUR, NGN, KES, ETB
amount_crypto NUMERIC(20,8) -- BTC, ETH, USDT
Never DECIMAL, never DOUBLE PRECISION, never REAL.
occurred_at TIMESTAMPTZ NOT NULL
Never TIMESTAMP (without time zone). Never DATE for event times.
If the table is tenant-scoped, add tenant_id BIGINT NOT NULL and enable RLS:
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
ALTER TABLE <table> FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON <table>
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
Use TEXT with CHECK constraints, not native ENUM types (easier to evolve):
status TEXT NOT NULL CHECK (status IN ('PENDING','ACTIVE','SUSPENDED','CLOSED'))
src/main/resources/db/changelog/changes/sql/<scope>-<NNN>-<description>.sql
Example: ledger-001-core-tables.sql, payment-link-002-add-expires-at.sql.
--liquibase formatted sql
--changeset <author>:<scope>-<NNN>-<description>
Example:
--liquibase formatted sql
--changeset payserflow:payment-link-001-schema
CREATE TABLE payment_links (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
amount NUMERIC(20,2) NOT NULL CHECK (amount > 0),
currency TEXT NOT NULL,
description TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('ACTIVE','EXPIRED','CANCELLED','USED')),
metadata JSONB NOT NULL DEFAULT '{}',
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_modified_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_payment_links_tenant_status ON payment_links(tenant_id, status);
CREATE INDEX idx_payment_links_expires_at ON payment_links(expires_at)
WHERE status = 'ACTIVE';
ALTER TABLE payment_links ENABLE ROW LEVEL SECURITY;
ALTER TABLE payment_links FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON payment_links
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
--rollback DROP POLICY tenant_isolation ON payment_links;
--rollback ALTER TABLE payment_links DISABLE ROW LEVEL SECURITY;
--rollback DROP INDEX idx_payment_links_expires_at;
--rollback DROP INDEX idx_payment_links_tenant_status;
--rollback DROP TABLE payment_links;
--liquibase formatted sql
--changeset payserflow:payment-link-002-add-reference
ALTER TABLE payment_links ADD COLUMN reference TEXT;
CREATE UNIQUE INDEX idx_payment_links_reference ON payment_links(tenant_id, reference)
WHERE reference IS NOT NULL;
--rollback DROP INDEX idx_payment_links_reference;
--rollback ALTER TABLE payment_links DROP COLUMN reference;
--liquibase formatted sql
--changeset payserflow:payment-link-003-backfill-status-batch
--runInTransaction:false
DO $$
DECLARE
batch_size INT := 10000;
rows_updated INT;
BEGIN
LOOP
UPDATE payment_links
SET status = 'EXPIRED'
WHERE id IN (
SELECT id FROM payment_links
WHERE status = 'ACTIVE' AND expires_at < now()
LIMIT batch_size
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
PERFORM pg_sleep(0.05); -- throttle
END LOOP;
END $$;
--rollback SELECT 1; -- backfill is irreversible; document in ADR
src/main/resources/db/migration/V20260415_100000__create_payment_links.sql
Format: V<yyyyMMddHHmmss>__<snake_case_description>.sql. Use the current timestamp (not sequential numbers) to avoid merge conflicts.
-- Create payment_links table (multi-tenant, RLS-protected)
CREATE TABLE payment_links (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
amount NUMERIC(20,2) NOT NULL CHECK (amount > 0),
currency TEXT NOT NULL,
description TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL CHECK (status IN ('ACTIVE','EXPIRED','CANCELLED','USED')),
metadata JSONB NOT NULL DEFAULT '{}',
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_modified_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_payment_links_tenant_status ON payment_links(tenant_id, status);
CREATE INDEX idx_payment_links_expires_at ON payment_links(expires_at)
WHERE status = 'ACTIVE';
ALTER TABLE payment_links ENABLE ROW LEVEL SECURITY;
ALTER TABLE payment_links FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON payment_links
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
src/main/resources/db/migration/U20260415_100000__create_payment_links.sql
DROP POLICY tenant_isolation ON payment_links;
ALTER TABLE payment_links DISABLE ROW LEVEL SECURITY;
DROP TABLE payment_links;
-- Flyway: mark this migration as transactional=false
-- Add ";-- FLYWAY TRANSACTIONAL=false" header or set spring.flyway.mixed=true
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_payment_links_created_at
ON payment_links(created_at);
SELECT COUNT(*) FROM <table> (or ask user to run it)SELECT * FROM pg_depend WHERE refobjid = '<table>'::regclassCONCURRENTLY. Never block writes on production.NOT VALID then VALIDATE CONSTRAINT.
ALTER TABLE child ADD CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES parent(id) NOT VALID;
ALTER TABLE child VALIDATE CONSTRAINT fk_parent; -- runs without exclusive lock
SELECT 1; -- irreversible, see ADR.pg_sleep(0.05) between batches) and run in non-transactional mode.ON CONFLICT DO NOTHING — seeds must be idempotent.After creating a migration, generate a verification SQL file that asserts the expected state:
-- verify-payment-link-001.sql
SELECT assert_equal('payment_links table exists',
(SELECT COUNT(*) FROM information_schema.tables
WHERE table_name = 'payment_links'), 1);
SELECT assert_equal('tenant_id column exists',
(SELECT COUNT(*) FROM information_schema.columns
WHERE table_name = 'payment_links' AND column_name = 'tenant_id'), 1);
SELECT assert_equal('RLS enabled',
(SELECT relrowsecurity FROM pg_class WHERE relname = 'payment_links'), TRUE);
SELECT assert_equal('check constraint on amount',
(SELECT COUNT(*) FROM pg_constraint
WHERE conname LIKE 'payment_links%amount%check'), 1);
produces:
- type: "migration"
format: "sql"
paths:
- "src/main/resources/db/changelog/changes/sql/<scope>-<NNN>-<description>.sql" # Liquibase
OR
- "src/main/resources/db/migration/V<timestamp>__<description>.sql" # Flyway
- "src/main/resources/db/migration/U<timestamp>__<description>.sql" # Flyway undo (optional)
- type: "verification"
format: "sql"
paths: ["docs/migrations/verify-<description>.sql"]
- type: "doc"
format: "markdown"
path: "docs/migrations/<description>.md" # rollback plan, risks, expected downtime
handoff: "Write claudedocs/handoff-db-migration-<timestamp>.yaml — suggest: verify-impl, api-first (if paired with endpoint)"
SERIAL / BIGSERIAL / IDENTITY / GENERATED ALWAYS AS IDENTITY PKs — use application-generated TSIDsCREATE TYPE ... AS ENUM — use TEXT + CHECK constraintsTIMESTAMP without time zone — always TIMESTAMPTZDECIMAL, DOUBLE PRECISION, REAL, MONEY — always NUMERIC(20,2) or NUMERIC(20,8)DROP TABLE ... CASCADE without rollback script — irreversibleCONCURRENTLY on populated tables--rollback section in LiquibaseNOT VALID on large tablesON CONFLICT DO NOTHING--liquibase formatted sqltools
Use this skill to verify a completed implementation through live testing — API calls, database state checks, and UI automation with Playwright. Triggers include: "test the implementation", "verify this works", "run API tests", "check the database", "test the UI", "end-to-end verify", "smoke test", "sanity check the implementation", "manually test", or any time an implementation needs post-build validation beyond unit tests. Also triggered automatically by spec-to-impl during the integration review phase. Use this when you want real evidence the system works — not just that tests compile. Can consume a pre-generated e2e/test-plan.yaml from spec-to-impl for fully automated test execution.
development
--- name: ux-review description: Evaluate a UI/UX design or implementation using heuristic analysis, accessibility audit, and cognitive walkthrough. Triggers: "UX review", "usability review", "heuristic evaluation", "accessibility audit", "is this usable". argument-hint: "[feature / screen / URL / mockup]" effort: high --- # UX review ## What I'll do Evaluate a design or implementation for usability, accessibility, and user experience quality using established heuristic frameworks. ## Inputs
development
--- name: user-flow description: Map user journeys through a feature or product, identifying key paths, decision points, friction, error states, and edge cases. Triggers: "user flow", "user journey", "flow diagram", "happy path", "user path". argument-hint: "[feature / user goal]" effort: medium --- # User flow ## What I'll do Map the complete user journey for a feature — from entry point through completion — including happy paths, error states, edge cases, and decision points. > **user-flow
development
Use this skill to produce complete UI/UX design artifacts from a specification document or panel analysis. Triggers include: "design the UI for this spec", "create wireframes", "design this panel", "UX design from spec", "generate component specs", "design tokens", "create the UI design for", "design system for", "wireframe this feature", "design a UI", "create a design system", "design this component", "design the layout", "create a style guide", "design a screen", "UI/UX review", "typography system", "color system", "spacing system", "design this feature", "design the dashboard", "design the onboarding", "create a component library", "design review", "audit the design", "improve the UI", "redesign this", "design system documentation", "create design guidelines", "responsive design", "mobile design", "dark mode design", "design the brand", or any time a spec/panel analysis document needs to be transformed into actionable UI/UX deliverables before implementation. Also triggers for standalone design system creation, component design, design reviews, dark mode/responsive variants, and developer handoff — even before code is involved. Orchestrates a multi-agent design team (UX Lead, UI Designer, Component Architect, Accessibility Reviewer, Design System Engineer, Design Reviewer) in parallel waves. Outputs feed directly into spec-to-impl's FE agent and figma-to-code.