Skip to content
PtahPtah

Serve a live schema view

ptah schema serve currently reads Go annotations only for the desired schema. It does not accept a schema file or external loader. Use generated HTML schema documentation for a source-neutral browsable reference, or ptah schema drift for a source-neutral live-database check with a pipeline exit code.

ptah schema serve serves a read-only web page carrying two things: the desired schema, and how a live database differs from it. The page re-reads both on every request, so where ptah schema compare answers once, this keeps answering while you work.

Use it as a second window beside the editor while you are changing models. It is not a pipeline tool: a pipeline wants an exit code, which ptah schema drift gives it. Nothing here writes to the database, and no migration depends on the server having run.

The live Shop schema view showing zero differing categories and the message that the database matches the declared schema.

The live view before and after one desired column diverges from the SQLite database.

What to notice: Both states use the same framing: the verdict and severity counters stay above the schema diagram, while the drift state adds the exact differing category.

Reproduce this output
ptah schema serve --root-dir docs/site/fixtures/schema-ui/internal/models --db-url sqlite://shop.db --addr 127.0.0.1:7070 --refresh 0 --title 'Shop schema'

Prerequisites:

  • A ptah binary on your machine (Install Ptah).
  • A desired schema written as Go annotations under a directory. schema serve reads that source and no other; see Limitations.
  • The URL of the database to compare against.

The examples use two annotated models and a local SQLite database that already matches them. Save this as models/schema.go:

package models
//ptah:schema:table name="customers"
type Customer struct {
//ptah:schema:field name="id" type="INTEGER" primary="true"
ID int
//ptah:schema:field name="email" type="TEXT" not_null="true" unique="true"
Email string
}
//ptah:schema:table name="orders"
type Order struct {
//ptah:schema:field name="id" type="INTEGER" primary="true"
ID int
//ptah:schema:field name="customer_id" type="INTEGER" not_null="true" foreign="customers(id)"
CustomerID int
}

Create the database from it:

Terminal window
ptah schema apply --root-dir ./models --db-url "sqlite://$PWD/shop.db" --auto-approve

Substitute your own model directory and database URL throughout.

Terminal window
ptah schema serve --root-dir ./models --db-url "sqlite://$PWD/shop.db"

Expected output includes:

Serving a read-only schema view on http://127.0.0.1:7070

Open the printed address in a browser. The address is read back from the listener rather than echoed from the flag, so --addr 127.0.0.1:0 prints the port that was actually chosen.

The page has two parts: a status panel this command adds, and the schema itself.

The status panel carries four counters — differing categories, destructive, warning, safe — followed either by the sentence The database matches the declared schema. or by a Drift table. Under it sits a timestamp reading compared and a UTC time, because a live view whose age is unknown is not a live view.

The matching view keeps the status decision above the diagram and table reference, so the first viewport answers whether action is needed.

Drift column What it holds
Category The difference category, for example columns_added.
Objects How many objects fall in that category.
Severity safe, warning, or destructive.

Those are the categories and severities ptah schema drift reports, from the same classification.

Below the panel is the schema, rendered by the same code as ptah schema export --to html: an entity diagram, then one section per table with its columns and indexes. The sidebar names the page, the database address with any credentials removed, and each table.

The models and the database are read again for each request, so a change to either shows up on the next page load with no restart. Add this field to the Order struct in models/schema.go:

//ptah:schema:field name="placed_at" type="TEXT"
PlacedAt *string

Reload the page. The counters read one differing category, of which one is a warning, and the Drift table gains a row: columns_added, 1 object, severity warning. The new column also appears in the orders section below, because that section renders the desired schema rather than the database.

The page reloads itself every 30 seconds through a meta refresh tag and carries no JavaScript. --refresh 15s shortens the interval; --refresh 0 serves a page that does not reload.

  • --title "Shop schema" replaces the default title, Schema dashboard, in the browser tab, the heading and the sidebar.
  • --addr selects the listen address. The default is 127.0.0.1:7070.
  • --refresh sets the self-reload interval as a Go duration, and 0 disables it.
  • --root-dir is repeatable, so a schema split across directories is served from one page.
  • --schemas limits the database read to the named schemas, comma-separated.

Each of these also reads an environment variable, printed on its --help line: PTAH_TITLE, PTAH_ADDR, PTAH_REFRESH, PTAH_ROOT_DIR, PTAH_SCHEMAS.

There is no JSON endpoint. The HTML is the only machine-readable answer, and every path returns the same page, so curl on / is a complete client:

Terminal window
curl -s -o dashboard.html -w '%{http_code}\n' http://127.0.0.1:7070/
curl -s -X POST http://127.0.0.1:7070/

Expected output includes:

200
this dashboard is read-only

GET and HEAD answer; every other method gets 405 with the header Allow: GET, HEAD. The refusal is applied before any route runs, so the surface stays read-only whatever is added to it. Responses carry Cache-Control: no-store, since a cached copy would show drift that has since been fixed.

For a page you scrape, add --refresh 0. The meta tag is then absent and nothing else about the response changes.

Press Ctrl-C. Ptah writes one line to stderr and exits 130:

interrupt received, releasing resources; interrupt again to stop immediately

The database cannot be reached. The response stays at HTTP 200 and the counters are replaced by a banner headed The database could not be compared, the driver’s error, and last attempt with a UTC time. Zero drift is not rendered in that case, because a page reading zero would tell a reader their schema is in sync when nothing was measured. The schema section keeps the last comparison that succeeded, and before the first success there is no schema section at all.

Three inputs stop the command before it listens, each with exit code 2:

Message on stderr Cause
error: database URL is required No --db-url, and no url: in a project config in the working directory.
error: listen on 127.0.0.1:7070: listen tcp 127.0.0.1:7070: bind: address already in use Another process holds the address. Pass a different --addr.
error: unknown flag: --schema-file A flag schema serve does not register. Run ptah schema serve --help for the set it does.
  • Go annotations are the only desired-schema source. --schema-file is not registered, so a YAML, HCL, SQL, DBML or oci:// schema cannot be served. The reason is in the long help: such a file may name an oci:// artifact, and a process that re-reads on a timer would put a registry request on a schedule. ptah schema drift reads those sources and answers once.
  • Without --root-dir, the working directory is scanned. Started where no annotated Go file exists, the desired schema is empty and the whole database reads as drift to remove: against the database built above, the panel reports 2 differing categories with 2 destructive, rows constraints_removed (4 objects) and tables_removed (2 objects), and a sidebar reading 0 tables. Nothing on the page says the desired side was empty.
  • The drift panel counts only the categories the safety classification names. A view declared in the models and missing from the database renders 0 differing categories here, while ptah schema compare reports views_added (1) for the same pair.
  • The schema section draws tables, columns, indexes, enums and foreign-key relations. Views, functions, triggers, sequences, row-level security (RLS) policies, roles and grants are not drawn.
  • Every path serves the dashboard. There is no health endpoint and no static asset path, so a monitoring probe on /healthz receives a full schema render at 200, and a reverse proxy cannot route by path.
  • The server has no authentication. The default 127.0.0.1:7070 keeps the page on the machine that runs it, and binding wider publishes an unauthenticated read of the schema.
  • There is no --config or --env flag. A ptah.yaml or atlas.hcl in the working directory is still read, and a malformed one stops the command before it listens, but neither can be pointed elsewhere.

Run ptah schema serve --help for every flag with its default and its environment variable, and for the long-form note on --schema-file. The process runs until it is interrupted: the inputs above exit 2 before it listens, and Ctrl-C exits 130. Exit codes carries the verb’s row and the convention behind those numbers, and A live view carries the flag table.