_____ _ _ | __ \| |_ __ _| |__ | |__) | __| / _` | '_ \ | ___/| |_ | (_| | | | | |_| \__| \__,_|_| |_|
Database migrations without surprises.
Ptah compares the schema you want with a live database and either writes versioned migrations or applies an approved plan. It also migrates persistent inference state, such as re-embedding a pgvector table. PostgreSQL, MySQL, MariaDB, SQLite and more. Open source, MIT, no toolchain to install.
$ curl -fsSL https://ptah.run/install.sh | sh
- Fully open source, MIT
- Zero telemetry
- Runs air-gapped
- Why that matters →
# Write the database's own schema into a file.$ ptah db read --db-url sqlite://app.db | tee schema.sqlCREATE TABLE "users" ( "id" INTEGER PRIMARY KEY, "email" TEXT NOT NULL); # Drift compares that file with the database it came from.$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbNo schema drift detected. # Now ask for a column, by rewriting the schema you want.$ cat > schema.sql <<'SQL'CREATE TABLE "users" ( "id" INTEGER PRIMARY KEY, "email" TEXT NOT NULL, "created_at" TEXT);SQL # The same check now has something to report.$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_added: 1 (warning) # Ask what it would take to close it. A dry run executes nothing.$ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-runPlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT; # Run the reviewed plan. --auto-approve suits a disposable file.$ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --auto-approvePlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT;Auto-approval enabled; applying schema changes.Schema apply completed successfully. # And the same check, a third time.$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbNo schema drift detected.
CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL);
CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL,+ created_at TEXT);
CREATE TABLE users ( id INTEGER PRIMARY KEY− email TEXT NOT NULL);
CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL);+ +CREATE TABLE sessions (+ id INTEGER PRIMARY KEY,+ user_id INTEGER NOT NULL,+ token TEXT NOT NULL,+ FOREIGN KEY (user_id) REFERENCES users(id)+);
$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-runPlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-runPlanned schema changes:-- Disable foreign-key enforcement for the table rebuild belowPRAGMA foreign_keys = off;-- SQLite table rebuild for changes ALTER TABLE cannot express on usersCREATE TABLE "__ptah_rebuild_users" ( "id" INTEGER PRIMARY KEY);INSERT INTO "__ptah_rebuild_users" ("id") SELECT "id" FROM "users";DROP TABLE "users";ALTER TABLE "__ptah_rebuild_users" RENAME TO "users";-- Restore foreign-key enforcement after the table rebuildPRAGMA foreign_keys = on;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-runPlanned schema changes:CREATE TABLE "sessions" ( "id" INTEGER PRIMARY KEY, "user_id" INTEGER NOT NULL, "token" TEXT NOT NULL, CONSTRAINT "fk_sessions_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id"));exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url postgres://app@db/appNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \ --db-url postgres://app@db/app --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url postgres://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url postgres://app@db/app --dry-runPlanned schema changes:-- Add/modify columns for table: users ---- ALTER statements: --ALTER TABLE "users" ADD COLUMN "created_at" TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url postgres://app@db/appSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url postgres://app@db/app --dry-runPlanned schema changes:-- Remove columns from table: users ---- ALTER statements: --ALTER TABLE "users" DROP COLUMN "email" CASCADE;-- WARNING: Dropping column users.email with CASCADE - This will delete data and dependent objects! --;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url postgres://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url postgres://app@db/app --dry-runPlanned schema changes:-- POSTGRES TABLE: sessions --CREATE TABLE "sessions" ( "id" INTEGER PRIMARY KEY NOT NULL, "user_id" INTEGER NOT NULL, "token" TEXT NOT NULL);-- ALTER statements: --ALTER TABLE "sessions" ADD CONSTRAINT "fk_sessions_user_id" FOREIGN KEY ("user_id") REFERENCES "users"("id");exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url mysql://app@db/appNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \ --db-url mysql://app@db/app --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url mysql://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url mysql://app@db/app --dry-runPlanned schema changes:-- Modify table: users ---- ALTER statements: --ALTER TABLE `users` ADD COLUMN `created_at` TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url mysql://app@db/appSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url mysql://app@db/app --dry-runPlanned schema changes:-- Modify table: users ---- ALTER statements: --ALTER TABLE `users` DROP COLUMN `email`;-- WARNING: Dropping column users.email - This will delete data! --;exit 0
$ ptah schema drift --schema-file schema.sql \ --db-url mysql://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \ --db-url mysql://app@db/app --dry-runPlanned schema changes:-- MYSQL TABLE: sessions --CREATE TABLE `sessions` ( `id` INTEGER PRIMARY KEY, `user_id` INTEGER NOT NULL, `token` TEXT NOT NULL);-- ALTER statements: --ALTER TABLE `sessions` ADD CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`);exit 0
One comparison. Every workflow reads it.
Whatever you write the schema in, Ptah parses it into one desired schema and diffs it against the database. Drift, plans, lint and apply are four readings of that one diff — and every check exits 0, 1 or 2, so CI never mistakes a broken connection for a clean schema.
schema.sqlschema.yamlschema.hclschema.dbmlmodels/*.goorm loader · stdout- desired schemalive database
- compare
schema driftthe proof 0 1 2schema plan · --dry-runthe review surfacemigrations lintthe gateschema apply · migrations upafter approval → live database
Three things it does in a typical week.
Real commands, real output. Exit codes are stable across all of them: 0 success, 1 a negative check result, 2 a usage or connection failure.
Keep unsafe and destructive migrations out of main.
- uses: stokaro/ptah-action@v1 with: dir: ./internal/models db-url: ${{ secrets.PTAH_DATABASE_URL }} dialect: postgres migration-dir: ./migrations $ ptah migrations lint --dir ./migrations \ --dialect postgres migrations/1790114595_drop_bio.up.sql:1 [warning] BC104: dropping a column retires a name application versions already deployed against the old schema still select and insert, so each of them starts failing the moment this migration commits, whether or not the column held any rows; deploy readers that no longer use the column first, then drop it in a later release (dropped column breaks deployed code) migrations/1790114595_drop_bio.up.sql:1 [error] DS102: DROP COLUMN permanently deletes the column's data; deploy readers that no longer use the column first, then drop it in a later release (column dropped) 2 finding(s).
Ptah destructive-change verdictfailurePtah found destructive migration statements Safety verdict: destructive. Destructive changes: yes.
The action plans the migration against the target database, posts the plan as a comment and writes one check run that fails when a statement would destroy data. ptah migrations lint exits 1 on a finding and 2 when it could not run, so a broken connection never looks like a clean directory.
Keep unreviewed migrations away from the database.
$ ptah migrations hash --dir ./migrations Wrote ./migrations/ptah.sum 4 migration file(s) hashed $ cat ./migrations/ptah.sum h1:wmq3WUjBVQT+Ll4SPPan7wJGghKMtqi2yDc2Tc4AKnM= 1790114505_create_users.down.sql h1:3hAVcH5B1mghhsNEDFiyIlGOdernvs+6dDRkaO79/F0= 1790114505_create_users.up.sql h1:WJmUNv9ET7Qaypt2hpFNHHrqSQ5XGu0Yi0gMV0gLEMo= 1790114510_add_created_at.down.sql h1:3E0vyUoTX+11C374nPdXruUtJycoXTRoGvl8XsHLLIk= 1790114510_add_created_at.up.sql h1:9wID/+fArtJnyBz/JvM/NDDd1Ovm5SvkaJYcygIXLc4=
$ ptah migrations up \ --db-url sqlite://app.db \ --migrations-dir ./migrations error: migration sum verification failed: migration directory does not match ptah.sum: changed: 1790114505_create_users.up.sql $ echo $? 2
ptah.sum records a checksum per file. up, validate and lint check it before they do anything, and status does with --verify-sum. What CI reviewed is what production runs.
Switch embedding models without a search outage.
run g1: cut_over, runningerror: not found: run g2run g1: cut_over, runningrun g2: boundary_captured, runningrun g1: cut_over, runningrun g2: backfilled, runningrun g1: cut_over, runningrun g2: caught_up, runningrun g1: cut_over, runningrun g2: indexed, runningrun g1: cut_over, runningrun g2: verified, runningrun g1: cut_over, runningrun g2: cut_over, running$ ptah inference plan --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85 source.table = docs (configured: the specification) model.identifier = ptah-docs-stub-v2 (configured: the specification) model.revision = 1 (configured: the specification) provider.credential = (configured: the specification, as a reference rather than a value) generation.reproducibility = full (inferred: the specification pins an immutable model revision) source.estimated_rows = 3 (measured) run.consistency_mode = outbox (configured: the migration specification) target.exists = false (measured) target.capability.vector_type = true (measured) target.capability.vector_index = true (measured) [prepare] create the vector column and metadata for generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85 [capture] install the outbox mechanism and record its starting position [backfill] embed 3 in-scope source rows [catch-up] process every source change recorded after the snapshot began [index] build the vector index and wait for it to be valid [verify] run every verification layer against the exact target generation [cutover] point queries at generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85, which nothing currently reads What is not established: - generation.reproducibility = full (inferred: the specification pins an immutable model revision) What leaves the database: - ptah-docs-stub-v2 at http://embeddings:8080/v1, declared local - the text of title, body - for 3 rows Consistency mode: outbox - the outbox event and the source change are one transaction, so a change that committed has an event
$ ptah inference prepare --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 prepared run g2 for generation 4f8a8e77ec39 - snapshot boundary: 788
$ ptah inference backfill --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --batch-rows 10 backfill finished: 3 scanned, 3 embedded, 0 skipped
$ ptah inference catchup --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --batch-rows 10 caught up to transaction 796: 0 changed rows, 0 tombstoned - the outbox keeps events this run has processed: floor 773 for public.docs is held by run g1 (generation b7cc9eb5c7d2ee13ad46ba3d5da8a479ec57b128d771e4151fab5bf6d1da0866)
$ ptah inference index --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 generation 4f8a8e77ec39 has a valid index
$ ptah inference verify --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85: 3 source rows, 3 target rows - every deterministic layer passed - not measured: the stored vectors were not read back, so their dimension was checked and their values were not; the target refuses a non-finite component on write, so nothing here could have carried one
$ ptah inference cutover --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 plan 949aa4c67b4b cutover refused: - this policy requires an approval and none was given error: cutover refused $ ptah inference cutover --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --approve 949aa4c67b4b --approver 'site check' queries now read generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85 (plan 949aa4c67b4b) - no stabilization window was asked for, so nothing is keeping the previous generation current and there is no rollback to it
Ptah builds the new generation beside the one queries read, calls your embeddings endpoint, verifies, and switches queries over when an approval names the plan. It never produces a vector itself, and plan tells you what leaves the database before anything runs.
Works with the schemas you already have.
Define the schema you want; review the changes needed to get there. Every source parses into the same internal schema before planning — files, Go annotations, a live database, or any program that prints one, an ORM included.
CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL, created_at TEXT );
tables: users: columns: id: type: INTEGER primary: true email: type: TEXT not_null: true created_at: type: TEXT
table "users" { column "id" { type = integer null = true } column "email" { type = text null = false } column "created_at" { type = text null = true } primary_key { columns = [column.id] } }
Table users { id integer [pk] email text [not null] created_at text }
package models //ptah:schema:table name="users" type User struct { //ptah:schema:field name="id" type="INTEGER" primary="true" ID int64 //ptah:schema:field name="email" type="TEXT" not_null="true" Email string //ptah:schema:field name="created_at" type="TEXT" CreatedAt *string }
$ ptah schema apply --schema-file schema.sql \ --db-url sqlite://app.db --dry-run Planned schema changes: ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.yaml \ --db-url sqlite://app.db --dry-run Planned schema changes: ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.hcl \ --db-url sqlite://app.db --dry-run Planned schema changes: ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.dbml \ --db-url sqlite://app.db --dry-run Planned schema changes: ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --root-dir ./models \ --db-url sqlite://app.db --dry-run Planned schema changes: ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
Any program that prints the complete schema as SQL, HCL or YAML to stdout is a source. Ptah runs it without a shell, bounds it, and refuses empty output.
- GORM
- SQLAlchemy
- Django
- TypeORM
- Sequelize
- Hibernate
- your script
a committed, version-pinned recipe in the repository · the others print SQL DDL and can use the same contract
$ ptah schema drift \ --schema-cmd ./scripts/export-schema \ --db-url sqlite://app.db No schema drift detected.
Lookup rows, declared next to the tables.
Ptah diffs the declared rows against the live table and writes a reversible data migration. Drift checks and direct plans read the same rows. A data migration that updates or deletes rows needs --allow-destructive.
//ptah:schema:data · countries.yaml- diff by key against the live table
INSERT · UPDATE · DELETE.up.sql / .down.sql · ptah.sum
Roles, grants and row-level policies, declared with the tables.
ptah schema security reads the live database and flags a table granted to a role with no row-level security. ptah viz --security marks the same finding on the declared schema. Gate a pull request on it with --fail-on any.
GRANT … TO roleptah schema security- PRV01 · no RLS on a granted table
- declare a policy
One schema, exported to whatever needs it.
Seven targets. Go annotations reach all seven; a schema file reaches every one but HCL. Contracts for machines, documents for people, two more schema formats. Nothing connects to a database, and every export is a candidate to review.
$ ptah schema export --to openapi-v3 --root-dir ./models
$ ptah schema export --to graphql --root-dir ./models \ --graphql-operations list,by-id
$ ptah schema export --to protobuf --root-dir ./models \ --proto-package shop.v1 --out shop.proto
$ ptah schema export --to markdown --root-dir ./models
$ ptah schema export --to html --root-dir ./models --out schema.html
$ ptah schema export --to dbml --root-dir ./models
$ ptah schema export --to hcl --root-dir ./models --out schema.hcl
openapi: 3.0.3 info: title: Ptah Exported Schema version: 1.0.0 servers: - url: / paths: {} components: schemas: products: type: object required: - id - name - price - status properties: id: type: integer format: int32 name: type: string maxLength: 255 price: type: number status: type: string enum: - active - inactive
# Code generated by ptah; DO NOT EDIT. # Operation shapes below were generated on request. They are type declarations # only: Ptah generates no resolvers, no data access, no authorization or tenant # isolation, and no filtering, ordering, pagination, or transaction behavior. enum ProductStatus { active inactive } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Query { products(first: Int, after: String): ProductConnection product(id: ID!): Product } type Product { id: ID! name: String! price: Float! status: ProductStatus! } type ProductEdge { node: Product! cursor: String! } type ProductConnection { edges: [ProductEdge!]! pageInfo: PageInfo! }
Exported Protobuf schema to /tmp/app/shop.proto Exported 1 message(s), 4 field(s), 1 enum(s) bootstrapped new compatibility history 2 export warning(s) reported $ cat shop.proto edition = "2023"; package shop.v1; message Product { int32 id = 1; string name = 2; string price = 3; ProductStatus status = 4; } enum ProductStatus { PRODUCT_STATUS_UNSPECIFIED = 0; PRODUCT_STATUS_ACTIVE = 1; PRODUCT_STATUS_INACTIVE = 2; } // Code generated by ptah schema export --to protobuf; DO NOT EDIT. // ptah:protobuf-export-version=2 // ptah:content-sha256=4b1c2203313b7090dd40822f26eda0ee8d0b1df0ffb61c3691aa34d06de3c81b
# Schema reference - [products](#products) ## products | Column | Type | Null | Default | Key | Comment | | --- | --- | --- | --- | --- | --- | | id | SERIAL | no | — | PK | — | | name | VARCHAR(255) | no | — | — | — | | price | DECIMAL(10,2) | no | — | — | — | | status | product_status | no | — | — | — | ## Enums - `product_status` — active, inactive
Exported schema documentation to /tmp/app/schema.html
Found 1 table(s), 4 field(s), 1 enum(s)Enum "product_status" { "active" "inactive" } Table "products" { "id" SERIAL [pk, not null] "name" VARCHAR(255) [not null] "price" DECIMAL(10,2) [not null] "status" product_status [not null] }
Exported HCL schema to /tmp/app/schema.hcl Found 1 table(s), 4 field(s), 1 enum(s) $ cat schema.hcl // Code generated by ptah; DO NOT EDIT. enum "product_status" { values = ["active", "inactive"] } table "products" { column "id" { type = SERIAL } column "name" { type = VARCHAR(255) } column "price" { type = DECIMAL(10,2) } column "status" { type = enum.product_status } primary_key { columns = [column.id] } }
api_name, api_type and api_expose shape the published contract without touching the migration engine — a DECIMAL can ship as a string, and a column can be left out of the contract. Protobuf field numbers persist in the file you commit.
Two ways to put a model on the project. One set of tools. Never the database.
Pick who owns the model connection. Everything downstream is the same Ptah: eleven tools with a workspace, every write previewed and checked against a digest, an audit line for each decision.
- read → preview → approve → applyThe approval covers one patch, named by its digest. If the files changed after the preview, the apply refuses.
- Allow once · for this session · NoYou answer in the terminal or in the client. Repository policy can only remove authority.
- Never writes to the databaseNo tool runs SQL, changes a database or applies a migration, and no setting adds one.
ptah migrations upstays a person’s step. - Audit and byte countWith a workspace, every permission decision goes to
.ptah/agent-audit.jsonl. In ptah assist, each answer ends with how many bytes of project content reached the provider.
↑ up • ↓ down • / filter • enter submit
> now write the migration for that column into the migrations directory in Ptah's native format, and call apply_patch for it. be brief Held — not written. Two blockers, both mine, not yours: • Format unknown: no tool result in this session has shown me Ptah's native migration file format, and I won't invent one. • No preview to apply: apply_patch needs a preview token/digest from a fresh preview_patch against the current migrations artifact (last listed: 0 files, digest sha256:70f6b4… ). I have none, and I can't claim a write or a verification pass without them. If the migration tool surface is enabled here, I'll: preview a migration adding tasks.active (matching the schema change already applied), show you the diff, then apply it with its token. -- AcmeQuant/Qwen3.8-27B via router, 1 turn(s), 0 tool call(s), answer -- No Ptah tool answered, so nothing above was checked against this project.
Fully open source. Zero telemetry. Runs where the network does not.
Built for data that has to stay where it is: regulated, sovereign, classified. Every component can be self-hosted, and none of them phones home.
- MITFully open source
The CLI,
ptah-compat,ptah-ls, the Kubernetes Operator, the GitHub Action and this site. Four repositories, one license, and no paid tier holds back a safety check. - PrivateZero telemetry
Ptah connects only to addresses you give it: your database, the embedding endpoint, the model provider for ptah assist, and the registry you push to or pull from. No usage pings, no update checks, no account.
- YoursSovereign by default
Schemas and migration directories as OCI artifacts in your own registry: ECR, GAR, Harbor, GHCR, Docker Hub or a plain
registry:2. The Operator runs in your cluster. Credentials are references, never values, so a spec is a file you can commit. - Air-gappedNo network required
Release archives hold ptah, ptah-compat and ptah-ls, built without CGO. Check an archive against
checksums.txtwith no network. For inference,--releaseaccepts an oci-layout directory you copied across on disk.
Before an inference run touches a provider, plan prints exactly what leaves: which model, which endpoint and its declared class, which columns, how many rows. It is the same list a data-protection review asks for.
What leaves the database: - ptah-docs-stub-v2 at http://embeddings:8080/v1, declared local - the text of title, body - for 3 rows Consistency mode: outbox - the outbox event and the source change are one transaction, so a change that committed has an event
Reconcile a database on a schema published as an OCI artifact.
The Ptah Operator pins the tag to a digest and uses only that digest. It verifies the artifact, observes the database and publishes a plan. By default it applies the plan only after an approval that names it, then observes the database again instead of trusting the job's exit code. Destructive plans are off by default; enabling them still binds the approval to the exact plan bytes.
- resolve tag → digest
- verify artifact
- observe database
- publish plan
- approve
- apply
- observe again
- 05
- destructive plans are off by default; enabling them binds the approval to the exact plan bytes
- 07
- the loop trusts what it observes in the database, not the apply job's exit code
Know what works on your database.
Ten engines. Pick yours to see what Ptah manages there, the limitation worth knowing, and which release lines are tested.
Manage tables, views, functions, extensions, custom types, roles and row-level security.
14 – 18Ptah-certified13Legacy-testedSchema changes, including automatic table rebuilds for alterations SQLite cannot express as ALTER TABLE.
3Ptah-certifiedPlanning that respects MySQL DDL rules: indexes, constraints, foreign keys and the SQL MySQL actually accepts.
8.4 · 9.7 · 26.7Ptah-certifiedMariaDB-specific DDL, indexes and constraints, planned with its own dialect rules.
10.11 · 11.4 · 11.8 · 12.3Ptah-certifiedSchema changes and row-level TTL policies, planned through the PostgreSQL path with CockroachDB rules.
25.4 · 26.2 · 26.3Ptah-certifiedSchema changes, roles and row-level security through the PostgreSQL path.
2024.2 · 2025.2 · 2026.1Ptah-certifiedT-SQL tables, filtered indexes and live catalog inspection.
2019 · 2022 · 2025Ptah-certifiedSchema inspection, planning and versioned migrations.
21 · 23Ptah-certifiedMergeTree tables with engine settings and sorting keys, materialized views and TTL policies.
26.3 · 26.7 · 26.8 · 26.9Ptah-certified24.10Legacy-testedSchema workflows through the PostgreSQL interface, tested against the Spanner emulator.
Check your server’s capabilities.
Reports the detected version, its support level and the capability profile Ptah will plan with.
$ ptah db capabilities --db-url "$DATABASE_URL"
Compatibility checks, backed by CI.
Automated probes run on every push to master and nightly: 28 Ptah-certified release lines — the lines Ptah tests in CI and commits to within their probed capabilities. Embedded SQLite and the Spanner emulator are declared as such, not as live servers.
View support evidence →ptah on the command line. ptah-ls in the editor for Go annotations. GitHub Action in CI, Kubernetes Operator in the cluster, Go packages at ptah.run in your own code.
ptah-compat accepts Atlas-shaped commands and runs Ptah behavior behind them. Independent clean-room implementation, not affiliated with Ariga. Atlas compatibility →