# onwardpg documentation
Source: https://onwardpg.solberg.is/index.md
onwardpg turns your desired PostgreSQL schema into SQL for a rolling deployment.
It generates **expand** SQL to run before the application deploys and **contract**
SQL to run after the old processes have drained. It verifies both in disposable
PostgreSQL databases. Your deployment system runs the SQL in production.
Jump to the [quick start](#quick-start), or read on for the deployment problems
it addresses.
## Why do I need this?
Locally, you can stop the app, migrate the database, and restart it. During a
rolling deployment, old and new instances share the same database. A worker
may still be processing yesterday's job while the new release serves requests.
A migration can execute successfully and still break those processes.
These changes need more than a single schema cutover:
- **Renaming a column.** Rename `display_name` to `full_name` before deploying,
and old queries fail. Deploy first, and new queries fail. For supported
same-type renames, onwardpg adds a second column and a trigger that keeps
writes through either name synchronized. You choose how to backfill existing
rows. Contract checks that the values agree and removes the bridge after old
callers drain.
- **Adding a required field.** `status text NOT NULL` can reject inserts from
old code that doesn't supply a status. Expand adds it as nullable. The new
release writes the field and tolerates nulls during rollout; contract runs
your cleanup, checks for remaining nulls, and applies `NOT NULL`.
- **Changing allowed values.** Suppose a CHECK accepts `email` and `slack`,
while the new release writes `slack` and `push`. Either endpoint constraint
rejects one version's writes. Expand can remove the old check; contract uses
your rule for retired values, checks the data, and restores enforcement.
That temporarily weakens the database guarantee, which the plan reports.
- **Changing a type beneath views and indexes.** Converting `age text` to
`age integer` needs a rule for blanks and malformed values, a way to support
both application versions, and work on dependent views. A materialized view
may also need rebuilding or refreshing. onwardpg identifies those
dependencies, asks for the conversion and compatibility SQL, and verifies
the completed bundle.
The order is **expand → deploy → drain → contract**. The new application must
handle the overlap schema and the final schema, including values old writers
can still produce. If that cannot fit one deployment, split the change across
releases. onwardpg asks for the missing decisions and SQL; it cannot infer
application behavior or a backfill's business meaning from the schema.
## Quick start
### Install
```sh
brew install jokull/tap/onwardpg
onwardpg version
```
This is a preview release. [Release archives](https://github.com/jokull/onwardpg/releases)
and [installation from Go or source](https://github.com/jokull/onwardpg/blob/main/docs/installation.md)
are also available.
You'll need PostgreSQL 15–18 for scratch databases. Use a dedicated local or CI
cluster: onwardpg creates and drops databases and temporary login roles there.
The scratch login needs permission to manage both. Never use a production or
shared application cluster for scratch.
For a local walkthrough with Docker:
```sh
docker run --name onwardpg-scratch \
-e POSTGRES_PASSWORD=onwardpg \
-p 127.0.0.1:55432:5432 -d postgres:18
export ONWARDPG_SCRATCH_DATABASE_URL='postgres://postgres:onwardpg@localhost:55432/postgres'
```
Wait for PostgreSQL to accept connections before continuing.
### Describe the starting schema
In an empty project directory, create `schema.sql`:
```sql
CREATE SCHEMA app;
CREATE TABLE app.bookings (
id bigint PRIMARY KEY
);
```
Add `.onwardpg.toml` alongside it:
```toml
version = 1
bundle_root = "migrations/onward"
[targets.app]
schema_file = "schema.sql"
scratch_database_env = "ONWARDPG_SCRATCH_DATABASE_URL"
```
Check the configuration and record the starting schema:
```sh
onwardpg config check
onwardpg init
```
`init` creates and verifies the baseline migration history. In an existing
project, initialize from the schema you have **before** the feature change.
It does not change your application database.
### Add a column
Suppose bookings now need a status. Replace `schema.sql` with:
```sql
CREATE SCHEMA app;
CREATE TABLE app.bookings (
id bigint PRIMARY KEY,
status text
);
```
The file always describes the complete desired schema. Run:
```sh
onwardpg plan add-booking-status
```
The bundle is written to `migrations/onward/app/add-booking-status/`. Its
`plan.json` lists the statements and hazards; `phases/expand.sql` contains:
```sql
ALTER TABLE "app"."bookings" ADD COLUMN "status" text;
```
There is no contract phase for this change. Old code can keep inserting rows
without a status. Check that inserts name their columns and that readers can
handle an extra result column if they use `SELECT *`.
### Review and verify
Read the generated SQL and hazards, then run:
```sh
onwardpg verify
```
Verification replays the migration in disposable databases, runs its assertions,
and checks that the final schema matches `schema.sql` with no remaining diff.
Unresolved SQL placeholders fail verification.
Commit the schema, configuration, and generated migration files together. In CI,
select the bundle explicitly and check its saved verification evidence:
```sh
onwardpg verify --bundle add-booking-status --check
```
Use the same PostgreSQL major for scratch and production. Scratch also needs
the roles, languages, and extension packages referenced by your schema. Project
SQL runs as a restricted database owner, so extensions requiring superuser
execution need separate provisioning.
## Required columns
Before merging the feature, suppose you decide every booking must have a status.
Change the column in `schema.sql` to `status text NOT NULL` and rerun:
```sh
onwardpg plan
```
This revises the same bundle. Expand still adds a nullable column: the old
application will omit `status` until it drains. The new application must write
a status and tolerate nulls when reading during that overlap.
The planner asks how those nulls will be resolved. Choose `assert_only` if
another operation will fill them, `manual_sql` to supply cleanup in this plan,
or `split_plan` to defer enforcement to a later release.
For this example, assume old bookings should become `pending` and the table is
small enough for one update. Supply that decision:
```sh
onwardpg plan \
--hint '{"kind":"reconcile","object":"column","name":["app","bookings","status"],"strategy":"manual_sql"}' \
--hint '{"kind":"manual_sql","action":"reconcile_contract_sql","object":"column","name":["app","bookings","status"]}'
```
The command reports `needs_sql_edits` and names the file to edit. In
`phases/contract.sql`, replace the placeholder **inside the edit markers** with:
```sql
UPDATE "app"."bookings"
SET "status" = 'pending'
WHERE "status" IS NULL;
```
Keep the surrounding generated SQL. After this cleanup it checks that no nulls
remain, then enforces the constraint:
```sql
ALTER TABLE "app"."bookings" ALTER COLUMN "status" SET NOT NULL;
```
Run `onwardpg verify` again. The cleanup rule is yours to review; the schema
cannot tell onwardpg what an old booking means. For a large table, use an
`operator_batched` operation with a completion check, or split the work across
releases. See the [contract reference](https://github.com/jokull/onwardpg/blob/main/docs/contract-readiness.md)
for those options.
## Day-to-day use
Run `onwardpg plan` as you edit the schema. It updates the active migration
instead of adding a migration for every draft. After rebasing onto new migration
history, run it again. Decisions and SQL edits carry forward when they still
apply; changes to their underlying schema can require another answer or review.
```sh
onwardpg plan # Revise the active plan
onwardpg status # Inspect it
onwardpg verify # Verify the current files
```
For renames, type conversions, and destructive changes, follow the questions in
`next_actions`. Answer with the reported `--hint` and edit only the named SQL
sections. Exit code `2` means the plan needs input or SQL edits. A rename needs
an explicit identity decision; a type conversion needs a rule for the data.
Adding another compatible column can introduce a new rename candidate, so a
previous rename decision may need confirming again. That keeps a changed set
of possible identities from silently reusing an earlier answer. Use the complete
suggested command (`argv`) for each answer: it can carry confirmed decisions
that have not yet been saved while your edited SQL waits for the remaining choices.
### Use your framework's schema
Instead of maintaining SQL by hand, export your framework's complete desired
PostgreSQL DDL. Replace `schema_file` with a command that writes it to stdout:
```toml
schema_command = ["pnpm", "--silent", "schema:export"]
```
`schema:export` is a script you provide. It must produce the same DDL for the
same models, including everything needed to build the schema from empty.
See the [schema input requirements](https://github.com/jokull/onwardpg/blob/main/docs/schema-inputs.md)
and [existing-project recipes for Drizzle, Prisma, and Django](https://github.com/jokull/onwardpg/tree/main/examples/frameworks).
Choose one executor for production DDL; running both framework migrations and
the onwardpg bundle would apply the same change twice.
Adopt from an already-deployed schema: export the old models and run `init`
before the feature edit. Keep existing framework migration history, but do not
replay the onwardpg baseline against an existing database. Django's example
exports migration state, so run `makemigrations` after model changes and use
reviewed state-only migrations when onwardpg takes over their DDL.
### Connect a development database
Optionally add these fields to `[targets.app]`:
```toml
dev_database_env = "ONWARDPG_DEV_DATABASE_URL"
dev_mode = "workspace"
```
Set that environment variable to your development database URL. onwardpg reads
its catalog and reports separate SQL to bring it up to date. In workspace mode,
objects left by another branch are preserved. Migration history still comes
from accepted bundles. The development and scratch databases must use the same
PostgreSQL major.
This comparison has its own `--dev-hint` decisions. A ready durable bundle can
still return exit code `2` because local reconciliation needs an answer; inspect
the `durable`, `development`, and `next_actions` sections separately.
### Work with a coding agent
Give the agent the onwardpg skill:
```text
Read and follow https://onwardpg.solberg.is/skill.md.
Plan this schema change, answer decisions from the application code,
verify the bundle, and report the remaining deployment checks.
```
## Deployment
Each bundle surrounds one application deployment:
```text
expand → deploy application → drain old processes → contract
```
1. **Apply expand** before deploying the new code. Review lock, scan, index-build,
and transaction requirements in the plan. Keep nontransactional batches out
of transactions.
2. **Deploy the application.** It must work both before and after contract.
3. **Drain old processes.** Include workers, scheduled jobs, queues, connection
pools, and any other client that could still write using the old schema.
4. **Check readiness, then apply contract.** Finish the specified data cleanup
and require a ready result before enforcing constraints or removing old interfaces.
For bundles with contract work, your release system supplies expiring writer
records in `deploy-readiness.json`. The file identifies the exact plan, bundle,
environment, and release. Check it against production through a restricted
read-only observer:
```sh
onwardpg contract check \
--target app \
--bundle add-booking-status \
--environment production \
--database-env PROD_READONLY_DATABASE_URL \
--evidence deploy-readiness.json
```
`ready` permits proceeding. `reconciliation_required` identifies cleanup your
release runner must complete before checking again. `needs_evidence`, `blocked`,
and `stale` need resolution. The [contract reference](https://github.com/jokull/onwardpg/blob/main/docs/contract-readiness.md)
defines the evidence file, observer permissions, and cleanup order. Contract SQL
repeats the data assertions at enforcement time.
Verification proves the migration runs and reaches the requested schema in
scratch. It does not prove your production data is valid, locks will be short,
replicas can keep up, or old processes have drained. Review those against the
actual workload. Plans are forward-only; arrange your application rollback
procedure before deploying.
## Reference
Use `onwardpg --help` for flags. The repository has the detailed
[CLI reference](https://github.com/jokull/onwardpg/blob/main/docs/cli.md),
[supported features](https://github.com/jokull/onwardpg/blob/main/docs/supported-features.md),
[bundle format](https://github.com/jokull/onwardpg/blob/main/docs/bundles.md), and
[safety model](https://github.com/jokull/onwardpg/blob/main/docs/safety-model.md).
onwardpg is [MIT licensed](https://github.com/jokull/onwardpg/blob/main/LICENSE).
[Source and issues](https://github.com/jokull/onwardpg) are on GitHub.
## Agent skill
Source: https://onwardpg.solberg.is/skill.md
---
name: onwardpg
description: Plan, revise, restack, verify, and assess contract readiness for PostgreSQL schema migrations with onwardpg. Use when a repository has .onwardpg.toml or the user asks to create or update an onwardpg plan, resolve planner decisions, author a backfill or compatibility transition, handle schema drift across branches, verify a migration bundle, or check post-expand readiness.
license: MIT
compatibility: Requires the onwardpg CLI, the repository's configured schema exporter, and access to its configured scratch PostgreSQL administrator. Development and production database access are optional and must remain separately scoped.
metadata:
author: onwardpg
version: "1.0"
---
# Plan PostgreSQL migrations with onwardpg
Use onwardpg as the catalog-aware planner and verifier. Bring application intent from the repository and the user; do not invent product semantics from a schema diff.
## Hard boundaries
- onwardpg writes reviewable migration bundles. It does not deploy them to production.
- Never run generated phase SQL against development, staging, or production unless the user explicitly asks for that separate operation.
- Never treat development reconciliation (`D -> W`) as the durable feature migration (`H -> W`).
- Never copy production rows, credentials, secrets, or sensitive values into prompts, bundles, logs, or verification fixtures.
- Supply a semantic hint only when application code, product requirements, or bounded data evidence proves it.
- Edit only files and `ONWARDPG TODO` pockets named by planner output. Preserve markers and generated ownership boundaries.
- Treat successful disposable verification as structural and semantic evidence, not proof of live data distribution, lock duration, traffic drain, replica health, or deployment completion.
- Treat `onwardpg contract check` as read-only, expiring readiness evidence. It
does not execute contract or replace the assertion repeated in contract SQL.
Read [the schema-state model](references/schema-states.md) before interpreting a diff. Read [production evidence](references/production-evidence.md) before querying any live or replicated database.
## Inspect before planning
1. Find `.onwardpg.toml` and read the target, PostgreSQL major, bundle root, schema exporter, and optional development database configuration.
2. Read the declarative schema source and the application readers and writers affected by the requested change.
3. Inspect existing bundles and repository delivery conventions. Do not create a second speculative migration for the same feature.
4. Run `onwardpg config check`. Fix configuration or export failures before interpreting planner output.
5. Run `onwardpg status` and `onwardpg history status`. If history is uninitialized, explain what `onwardpg init` will establish and obtain the user's intent before creating a baseline in an established project.
## Keep one feature plan alive
Start a new feature once:
```sh
onwardpg plan descriptive-feature-name
```
After every schema edit, answered decision, SQL edit, branch return, or rebase, revise the same active PlanID:
```sh
onwardpg plan
```
Do not stack local fixup migrations. Git moves the feature; rerunning `plan` restacks the same feature bundle on the currently accepted history and carries forward only decisions whose scoped meaning still holds.
Use JSON output, which is the default. Follow the high-level `status`, ordered
`next_actions`, nested decision choices, named edit requirements, and exit
codes rather than guessing. A `workspace_fast_forward` action contains direct
D -> W SQL plus exact argv; it is never the durable H -> W bundle. Use
`durable.status` as the effective bundle state. `durable.generated_plan` is
the raw generator result before edit reconciliation and may retain
`needs_sql_edits` as provenance after the edited artifact verifies.
Inspect `next_actions` even when the top-level status is `ready`: that state
means the durable artifact is ready, while caller-owned development SQL remains
an explicit optional action.
Lower-level `draft` reports additionally provide `next_action`:
- `0`: review the report and SQL; continue or verify.
- `2`: supply justified intent or edit only the named SQL pocket, then rerun.
- `3`: stop on unsupported catalog state; change the design, narrow an explicit ignore, or report the gap.
- `4`: repair stale history, receipts, residual differences, or clone evidence before continuing.
- `1`: fix invocation, configuration, export, connection, or environment errors.
See [the decision protocol](references/decision-protocol.md) for hints, SQL handoffs, branch switching, and rebases.
## Answer only what the repository proves
Hints contain semantic intent, never SQL or opaque planner fingerprints. An agent that already knows a rename or manual conversion strategy may provide it on the first invocation with `--hint` or `--hints-file`. onwardpg consumes only hints that match real dependency-ordered decisions and rejects stale, contradictory, impossible, or unused guesses.
Keep durable `--hint` decisions separate from local-only `--dev-hint` decisions. A messy development database is convenience evidence, not migration history.
For product-specific transformations:
1. Read all application paths that read or write the affected values.
2. Use bounded production aggregates or value-shape classifications only when the user has provided restricted read-only access.
3. Record any live precondition that must be rechecked during deployment.
4. Edit the named `expand.sql` or `contract.sql` pocket with reviewed SQL.
5. Resolve any planner-named Boolean contract-gate pocket in `contract.sql`;
that assertion is repeated at production enforcement time. Separately add
optional read-only assertions to `verify.sql` for synthetic `WITH ... VALUES
(...)` conversion examples or clone-only postconditions. Do not insert
fixtures into deployment phases or treat `verify.sql` as production
readiness evidence.
6. Rerun `onwardpg plan` when the declarative schema changes, then run `onwardpg verify` against the exact edited bundle.
## Development databases and branches
`onwardpg plan --output sql` emits only direct development reconciliation (`D -> W`). It is never the PR bundle. In workspace mode, preserve development-only objects that may belong to another branch.
When ordinary plan output contains a `workspace_fast_forward` action, inspect
its `preserved` objects and reason. `accepted_history_changed` means a rebase
left D behind the new accepted head. The included SQL is available immediately;
the argv renders the same stream for optional execution and repeats any
consumed ephemeral dev hints. Never copy D-only
objects into the durable plan, and stop if the development report asks a
decision or reports an incompatibility.
If a branch switch removes the active bundle from the checkout, name the returning or other plan explicitly. Let onwardpg park or restore worktree-local PlanIDs. If it reports an active-plan conflict, stop and resolve which feature is present instead of deleting anchors or fabricating another plan.
## Finish with an evidence handoff
Run:
```sh
onwardpg verify
onwardpg status
```
Report:
1. the PlanID and bundle path;
2. the H -> W compatibility strategy and application deployment assumption;
3. every consumed semantic decision and its code or product evidence;
4. edited phase SQL and synthetic verification assertions;
5. verification outcome and residual diff;
6. hazards, unsupported objects, or unanswered decisions;
7. live preconditions and operational gates still owned by the deployment system.
If the user is operating a post-expand deployment, validate provider-neutral,
expiring evidence for every potential writer cohort and run the read-only gate:
```sh
onwardpg contract check \
--environment production \
--database-env PROD_READONLY_DATABASE_URL \
--evidence deploy-readiness.json
```
Report `ready`, `needs_evidence`, `blocked`, or `stale`; never apply phase SQL.
Do not describe the migration as safe merely because clone verification passed.