Skip to content

Prisma Models, Ptah Deployment Checks

Keep Prisma models and its typed client, export the desired PostgreSQL schema, and use Ptah to plan, verify, and deploy a new ticket-priority field and index.

Ptah helps you plan, review, and apply database migrations. Try in your browser

A support application uses Prisma to describe tickets and query PostgreSQL. The next release adds ticket priority and an index for the urgent queue. We can keep the Prisma model and client while using Ptah to review and execute the database change.

The connection between the tools is a SQL schema exported by Prisma’s native CLI. Ptah compares that desired state with the database, generates migrations, and checks the result after deployment. No model-conversion script is needed.

This run used Prisma 7.10.0, Ptah 0.9.0, PostgreSQL 18.6, and Node.js 26.0.0. The example files include pinned dependencies, both model versions, client code, migrations, and captured output.

The initial model has an ID, subject, and status. We deployed that first schema through Ptah, generated the Prisma client, and inserted two tickets through the client. One was open; the other was closed.

The model change adds a default priority and an index:

examples/expected/model-change.prisma
priority String @default("normal")
@@index([status, priority], map: "tickets_status_priority_idx")

Export the complete desired schema:

Terminal window
npx prisma migrate diff --from-empty --to-schema schema.prisma --script > schema.sql

--from-empty is deliberate. This SQL describes everything the model wants, including the table that already exists. It is input to Ptah’s comparison; we do not execute that creation script against the existing application.

Let Ptah plan the change against the deployed database

Section titled “Let Ptah plan the change against the deployed database”

DB_URL names the existing database, and SHADOW_URL names a separate, disposable database for migration verification.

Terminal window
ptah migrations generate --db-url "$DB_URL" \
--schema-file schema.sql --schemas public \
--migrations-dir migrations --name ticket_priority \
--shadow-db "$SHADOW_URL"

Ptah generated two migration pairs. The first adds the column in a transaction:

examples/expected/add-priority.sql
ALTER TABLE "tickets" ADD COLUMN "priority" TEXT NOT NULL DEFAULT 'normal';

The second creates the index concurrently and declares that it must run outside a transaction:

examples/expected/add-index.sql
-- +ptah no_transaction
CREATE INDEX CONCURRENTLY IF NOT EXISTS "tickets_status_priority_idx" ON "tickets" ("status", "priority");

The complete files include their generated metadata. The statements above are the parts to review: existing tickets receive the default, and the index has its own execution mode. The shadow check replayed the history, applied both new files, rolled them back, and reapplied them successfully.

After reviewing the generated pairs, update the directory checksum and lint both new versions:

Terminal window
ptah migrations hash --dir migrations
ptah migrations lint --dir migrations --dialect postgres \
--latest 2 --fail-on error --dev-url "$SHADOW_URL"
ptah migrations up --db-url "$DB_URL" \
--migrations-dir migrations --verify-sum
ptah schema compare --db-url "$DB_URL" \
--schema-file schema.sql --schemas public --exit-code

Lint replayed the directory on the shadow database, so its checks read the schema each migration starts from. It reported no findings. Ptah applied the column change in a transaction and the concurrent index outside one. The final comparison found no schema differences. A separate empty database replayed all three versions, including the initial table migration, and reached the same schema.

Regenerate the client after changing the model:

Terminal window
npx prisma generate

Our application then marked the existing open ticket as urgent and created another urgent ticket. This query returned both:

examples/expected/query.ts
await prisma.ticket.findMany({
where: { status: "open", priority: "urgent" },
orderBy: { id: "asc" },
});

The results were ticket 1, “Invoice missing,” and ticket 3, “Export failed.” The original closed ticket remained with the default normal priority. These are actual Prisma client writes and reads against the Ptah-migrated database.

Compare state even when history is current

Section titled “Compare state even when history is current”

To test the deployment check, we deliberately dropped the index from the disposable application database. Ptah’s status still reported three applied migrations and zero pending files. Those migrations had run; their history was accurate.

Running the schema comparison again returned exit code 1 and identified tickets_status_priority_idx as missing. Restoring the index made the same comparison pass. A CI job can use that exit code to catch drift before an application release.

For a deployment, compare with the currently deployed model’s exported schema before applying a new change, then compare with the incoming model’s export afterward. Comparing an old database with tomorrow’s model would also report a difference, even when nobody changed the database outside migrations.

Prisma Migrate already provides migration generation and execution. Choosing Ptah here gives this application the same SQL review, migration linting, shadow verification, and live-schema checks used by services with other ORMs. In this run, Ptah also split the planned PostgreSQL change into transactional and concurrent-index files without a wrapper.

Prisma remains responsible for the model and typed client. Ptah owns migration execution for this database. Commit the model, exported schema, generated migrations, and checksum together so the reviewed database change travels with the client that uses it.

Put Ptah to work

Example files