ForgeTrust.AppSurface.Durable.PostgreSql
Source of truth
Source-only public preview: this package supplies explicit PostgreSQL schema management, Work, Flow, Schedule, an explicitly opted-in bounded runtime host, and durable W3C causal evidence. It remains excluded from publish plans until coordinated release review proves operational conformance. Storage registration itself starts no worker or hosted service.
Choose this package when an application must commit its domain mutation and durable Work acceptance in the same PostgreSQL transaction, and when process-loss recovery must use explicit leases, runtime/scope fences, effect permits, and provider-safety policy. Choose a larger workflow platform for arbitrary deterministic replay, child workflows, or unbounded fan-out. PostgreSQL is this provider's sole durable truth.
The verified database target is PostgreSQL 17.5.
The package references the adopter-facing
ForgeTrust.AppSurface.Durable contracts and the
ForgeTrust.AppSurface.Durable.Provider SPI. Neither package
depends on PostgreSQL.
Slice 7 discovery and reconciliation
This provider is a source-only public preview and remains held from publication pending coordinated release
evidence. Registration is passive: AddAppSurfaceDurablePostgreSql installs storage and runtime services but does not
start a worker, open a connection, or apply DDL. Continuous processing requires the explicit
AddWorkerHost() opt-in. Startup validates the installed schema and active epoch and never
applies DDL or advances migration history.
The production migration order is 0001_work_shared.sql, 0002_forced_rls.sql, 0003_flow_protocol.sql,
0004_schedule_protocol.sql, 0005_runtime_heartbeat.sql, and 0006_flow_trace_context.sql, followed by the
canonical Durable/configure-postgresql-roles.sql
role recipe. Prefer generating the Durable schema script offline, reviewing it, applying the forward-only migrations,
running the role recipe, and completing schema status/preflight before enabling the worker host. The
durable schema command family
keeps scripts offline while its online commands accept no connection-string argument and never print connection
strings. Its explicit apply --apply path resolves only a named migration-owner environment variable, normally
--connection-env APPSURFACE_DURABLE_MIGRATION_CONNECTION.
For reconciliation, check status first, create a corrected and reviewed forward-only script, and retry. Never delete
or rewrite migration history. The durable-postgresql example is a
local proof of these boundaries, not production operations guidance.
First proof
Run the source-evaluator slice 3 reference workload and
slice 4 reference workload to apply schema explicitly, accept Work and start Flows
atomically with domain mutations, force-terminate a separate process at the committed timer-winner checkpoint, and verify
the remaining recovery boundaries transactionally with fresh processors. The Work-first
Schedule protocol remains a bounded Gate A Work-only protocol check; its deterministic
crash-proof evidence remains deferred. Durable Flow trace context v1 documents the
value-free causal evidence and process-loss link proof. For the runtime activation boundary, use the worker-host path
below.
Explicit schema and epoch deployment
Construct PostgreSqlDurableRuntimeSchemaManager with a migration-owner NpgsqlDataSource:
GetStatusAsyncreports StoreId, nullable active epoch, migration state, and reader/writer compatibility;GenerateScriptproduces deterministic forward-only SQL from an exact reviewed installed version; generated SQL is not safe to rerun after a selected migration commits;ApplyAsyncapplies pending known migrations under one session advisory lock;InitializeRuntimeEpochAsyncactivates the first epoch exactly once; andRotateRuntimeEpochAsynccompare-and-swaps the active epoch after restore or an authorized recovery event.
Runtime mutations take a shared, transaction-scoped advisory fence before validating the active epoch. Schema changes and epoch rotation take the exclusive package lock, so they wait for in-flight runtime transactions and prevent an old epoch from committing new durable state after rotation.
Runtime roles never own schema or apply DDL. Apply the seven migrations in numeric order: Work/shared state
(0001_work_shared.sql),
forced RLS and privilege revocation (0002_forced_rls.sql), Flow protocol persistence (0003_flow_protocol.sql), the
Work-first Schedule ledger (0004_schedule_protocol.sql), the payload-free runtime heartbeat
(0005_runtime_heartbeat.sql), value-free Flow trace context (0006_flow_trace_context.sql), and verified one-Flow
retention lifecycle evidence (0007_flow_retention.sql). Applied schema is
forward-only; rolling application code back does not authorize destructive schema rollback. Execute generated SQL with a
client that stops on the first error; psql callers must pass -v ON_ERROR_STOP=1.
Create host principals outside migrations. Use configure-postgresql-roles.sql to
grant the migration-owner, payload-free dispatcher, scoped-runtime, and scoped-retention-operator capabilities. Service roles must not receive
ownership or BYPASSRLS. Transaction-local scope context is defense in depth, not a replacement for application
authorization. The recipe fails before granting privileges when role names alias each other or a service role can
inherit the migration owner, SUPERUSER, or BYPASSRLS. It also transfers every package table, sequence, and view to
the migration owner so pre-existing object ownership cannot preserve runtime DDL authority. Existing direct,
inherited, or PUBLIC schema, relation, column, and sequence privileges outside the documented allowlist cause the
transactional recipe to fail and roll back; remove those host-managed grants before retrying. The runtime role is
deliberately fully trusted for the unscoped runtime_heartbeat table: the health component owns the
worker-generation fence through its row lock and compare-and-swap predicates, so applications must not expose that
credential to untrusted callers.
Apply schema migrations before rerunning the role recipe. A migration can add package relations, but the recipe owns the reviewed grants for existing service roles; running it second is required before Flow runtime or dispatcher connections can use the new relations.
Run a worker host
After the schema is current, the recovery epoch is initialized, and the role recipe has run, compose exactly one runtime-role and one payload-free dispatcher-role data source. The warm path is designed to reach one hosted Work completion in five minutes; it never performs DDL at application startup.
using ForgeTrust.AppSurface.Durable.PostgreSql;
var workOptions = new PostgreSqlDurableWorkOptions(
runtimeEpoch,
expectedStoreId,
PostgreSqlDurableWakeNotificationMode.Enabled);
services.AddAppSurfaceDurablePostgreSql(
dispatcherDataSource,
runtimeDataSource,
workOptions,
new PostgreSqlDurableScheduleOptions("appsurface_durable_runtime"),
options =>
{
options.WorkerId = "orders-worker-01"; // unique for every live replica
options.MaximumItemsPerPass = 32;
options.TimeBudgetPerPass = TimeSpan.FromSeconds(10);
options.ShutdownReserve = TimeSpan.FromSeconds(5);
})
.AddWorkerHost();
AddAppSurfaceDurablePostgreSql resolves Work, Flow, Schedule, schema, pump, health, and drain services but installs
no IHostedService, opens no connection, and applies no migration. AddWorkerHost() is the standard continuous
activation path. On startup it validates schema compatibility and the active epoch, but never applies DDL or advances
migration history. It also validates that TimeBudgetPerPass + ShutdownReserve fits inside
HostOptions.ShutdownTimeout; an invalid store or host configuration fails closed.
The host calls the same IDurableRuntimePump
used by an external activator. A Pass runs at most MaximumItemsPerPass committed Turns and rotates Work, Flow, and
Schedule after each selected-surface attempt. Empty and deferred surfaces advance the cursor without consuming that
item budget. PostgreSQL remains authoritative for discovery, claims, leases, effect permits, Flow transitions,
Schedule facts, row-level security, and recovery epochs. Deploy separately identified workers when a long Work
invocation needs stronger latency isolation from Flow or Schedule.
Enabled notification mode creates one dedicated LISTEN appsurface_durable_wake connection. It coalesces and discards
metadata-only notification payloads; polling remains the recovery path for lost, duplicate, delayed, or unavailable
hints. A receipt never authorizes a claim.
Resolve IDurableRuntimeHealth through
an application-owned authorized health endpoint. Healthy is the only ready state; NotStarted, Stale, Draining,
and Incompatible are intentionally not ready. Snapshots contain aggregate counts and fixed codes only—never payload,
scope, aggregate, connection, or trace values. At shutdown, local admission closes synchronously before the host
persists drain; already-permitted Work follows its ordinary cancellation/recovery path rather than inventing a result.
For a cold path, apply 0005_runtime_heartbeat.sql with the migration owner, rerun
configure-postgresql-roles.sql,
verify the active epoch and StoreId, deploy with AddWorkerHost() disabled, then enable it. Roll back application code
by disabling the worker host and deploying a previous compatible binary; never destructively roll back a migration. If
Issue #685 supplies an intervening migration first, renumber this migration to the next contiguous number while preserving
its content and rerun the role recipe.
Role recipe contract
Run the recipe with psql as a principal that can transfer ownership and grant privileges:
psql -v ON_ERROR_STOP=1 \
-v migration_owner_role=appsurface_durable_owner \
-v dispatcher_role=appsurface_durable_dispatcher \
-v runtime_role=appsurface_durable_runtime \
-v retention_operator_role=appsurface_durable_retention \
-f Durable/configure-postgresql-roles.sql "$CONNECTION_STRING"
The dispatcher, runtime, and retention-operator values identify exact non-human credentials used to connect, not reusable capability
groups. All three must be distinct LOGIN leaf roles with no memberships in either direction and without SUPERUSER,
CREATEDB, CREATEROLE, REPLICATION, or BYPASSRLS. Create and rotate their credentials through the deployment
secret system; the recipe never accepts or changes passwords. Neither service credential may own any database or hold
grant options on the appsurface_durable schema or its objects. The migration owner remains separate and may be
NOLOGIN.
The appsurface_durable schema is package-reserved. The recipe serializes with migrations and runtime transactions,
then transfers every table, partition, sequence, view, materialized view, foreign table, and package function in that
schema to the migration owner. Do not place application-owned objects there.
| Principal | Allowed privileges |
|---|---|
| Dispatcher | Schema USAGE; global table SELECT on payload-free dispatch and flow_dispatch; EXECUTE only on the constrained Schedule claim function. It receives Schedule routing IDs and revision, never raw schedule_dispatch columns. |
| Runtime reads | Schema USAGE; table SELECT on package metadata, scoped Work, Flow, Schedule, and Flow trace-context relations. Dispatch reads are scope-filtered by transaction-local RLS. |
| Runtime inserts | Table INSERT on scoped Work, Flow, Schedule, and Flow trace-context relations. |
| Runtime updates | Reviewed column-level UPDATE on mutable Work, Flow instance/wait/timer, Schedule definition/occurrence/dispatch, and dispatch fields; no table-wide update grant. |
| Runtime sequences | USAGE and SELECT on every sequence in the package schema. |
| Runtime heartbeat | Unscoped SELECT and INSERT, plus reviewed column-level UPDATE, on runtime_heartbeat for IDurableRuntimeHealth. Its forced RLS policy intentionally uses USING (true) and WITH CHECK (true); keep this fully trusted runtime credential out of untrusted callers. |
| Retention operator | Scope-filtered Flow/Work-reference and retention-evidence reads; EXECUTE only on the owner-run manifest and lifecycle capabilities. It has no direct lifecycle/source INSERT, UPDATE, DELETE, sequence, dispatcher-discovery, Schedule, worker-host, or migration access. |
Neither service credential receives schema CREATE, table-wide UPDATE, DELETE, TRUNCATE, REFERENCES,
TRIGGER, or MAINTAIN; the dispatcher receives no sequence privileges. Forced RLS remains an additional scope fence,
not the reason destructive privileges are safe. The recipe also rejects disabled or unforced RLS and any policy whose
name, command, role target, permissiveness, USING, or WITH CHECK expression differs from the reviewed migration.
Before the pre-created next month starts, a migration-owner operation must run
SELECT appsurface_durable.ensure_schedule_history_partitions();. It retains the current and next Schedule-history
partitions and reapplies their forced RLS policy. Runtime and dispatcher roles cannot execute this maintenance function.
Verified Flow retention
Register retention only after the schema is current and the four-role recipe has completed. Supply a dedicated retention-operator data source; it must not be the dispatcher or runtime source:
services.AddAppSurfaceDurablePostgreSql(
dispatcherDataSource,
runtimeDataSource,
workOptions,
scheduleOptions);
services.AddAppSurfaceDurablePostgreSqlFlowRetention(retentionOperatorDataSource);
Migration 0007_flow_retention.sql installs PostgreSQL's trusted pgcrypto extension in public to recompute
canonical SHA-256 source-item evidence inside the owner-run capabilities. The migration owner therefore needs database
permission to install that extension on first use, or an operator must preinstall pgcrypto in public before applying
the migration. A pre-existing installation in another schema is rejected with an explicit migration error; move it to
public before retrying. The capabilities call the extension by schema-qualified identity; it is not resolved through a
caller-owned search path.
Resolve IDurableFlowRetentionClient only behind an application-authorized operator boundary. The lifecycle is
AssessAsync → CreateManifestAsync → BuildArchivePackageAsync → external write → RecordArchiveReceiptAsync →
VerifyArchiveAsync → optional SetHoldAsync → PurgeAsync. Assessment has no universal age policy: callers select
one terminal Flow and supply a maximum of 10,000 closure items and 64 MiB package bytes. The owner-run PostgreSQL
capabilities validate scope and lifecycle state, lock the source closure during verification and purge, retain the Flow
identity and command ledger, clear terminal payload fields, and delete
only manifest-covered Flow history, resolved waits, terminal timers, and terminal dispatch rows. A retry returns the
persisted command outcome; a changed source, stale sequence, active child Work, repair-required Flow, or hold rejects
the operation without a partial delete.
The retention login receives no direct mutation grants. CreateManifestAsync calls
appsurface_durable.create_flow_retention_manifest, while receipt, verify, hold, and purge call
appsurface_durable.apply_flow_retention_lifecycle. Both capabilities require the transaction's scoped identity,
serialize command replay, validate lifecycle sequencing, and compare every live source item's server-computed SHA-256
against the immutable manifest before verification or deletion. The application still owns authorization and the opaque
external-archive receipt: PostgreSQL proves source correspondence, not external storage availability or legal adequacy.
The application supplies command identity, actor, reason, and the expected lifecycle sequence for every mutation. A minimal orchestration shape is:
var assessmentResult = await retention.AssessAsync(
new DurableRetentionAssessmentRequest(scopeId, flowInstanceId),
cancellationToken);
if (!assessmentResult.IsSuccess ||
assessmentResult.Value.Status != DurableRetentionAssessmentStatus.Safe)
{
// Stop for Blocked or Indeterminate; do not create a manifest.
return;
}
var manifestResult = await retention.CreateManifestAsync(
new DurableRetentionManifestCreateRequest(
new DurableCommandId("retention-manifest-command"),
assessmentResult.Value),
cancellationToken);
if (!manifestResult.IsSuccess)
{
return;
}
var manifest = manifestResult.Value.Manifest;
var packageResult = await retention.BuildArchivePackageAsync(
scopeId,
manifest.ManifestId,
cancellationToken);
if (!packageResult.IsSuccess)
{
return;
}
var package = packageResult.Value;
await archiveStore.WriteAsync(package, cancellationToken);
var receipt = await archiveStore.CreateReceiptAsync(package, cancellationToken);
var sequence = manifest.LifecycleSequence;
var receiptResult = await retention.RecordArchiveReceiptAsync(
new DurableRetentionRecordArchiveReceiptRequest(
scopeId, manifest.ManifestId, new DurableCommandId("retention-receipt-command"),
actorId, "verified-flow-retention", sequence, receipt),
cancellationToken);
if (!receiptResult.IsSuccess)
{
return;
}
var verifyResult = await retention.VerifyArchiveAsync(
new DurableRetentionVerifyArchiveRequest(
scopeId, manifest.ManifestId, new DurableCommandId("retention-verify-command"),
actorId, "verified-flow-retention", sequence + 1),
cancellationToken);
if (!verifyResult.IsSuccess)
{
return;
}
var purgeResult = await retention.PurgeAsync(
new DurableRetentionPurgeRequest(
scopeId, manifest.ManifestId, new DurableCommandId("retention-purge-command"),
actorId, "verified-flow-retention", sequence + 2),
cancellationToken);
if (!purgeResult.IsSuccess)
{
return;
}
Treat each DurableOperationResult as a checked boundary: handle IsSuccess == false, preserve the returned
command outcome for retries, and stop on Blocked, Indeterminate, source changes, stale lifecycle sequences, or
holds. The archive write and receipt creation are external application steps; the provider never treats a receipt as
proof that external bytes are durable or legally adequate.
DFA1 package bytes are returned before external I/O. The archive receipt is an opaque adopter assertion, not a URI,
availability check, encryption proof, or compliance determination. Source-correspondence verification rebuilds the
canonical package and compares its SHA-256 and frozen closure digest. Applications must document and operate their own
archive store, key management, retention policy, legal holds, performance/WAL evidence, and recovery objectives.
Options reuse across Work and Flow
Create PostgreSqlDurableWorkOptions from the non-empty StoreId and explicitly active epoch returned by deployment. The options object is shared across Work and Flow operations:
internal static PostgreSqlDurableWorkOptions CreateSharedOptions(
Guid runtimeEpoch,
Guid expectedStoreId)
{
// PostgreSqlDurableWorkOptions is reused directly across Work and Flow operations to guarantee
// consistent ExpectedStoreId, active RuntimeEpoch, notification modes, and schema compatibility validation.
return new PostgreSqlDurableWorkOptions(
runtimeEpoch,
expectedStoreId,
PostgreSqlDurableWakeNotificationMode.Disabled);
}
Accept Work
using ForgeTrust.AppSurface.Durable;
using ForgeTrust.AppSurface.Durable.PostgreSql;
using Npgsql;
namespace DurablePostgreSqlConsumer;
internal static class PostgreSqlReadmeProof
{
internal static async ValueTask<DurableOperationResult<DurableWorkAcceptance>> AcceptAsync(
NpgsqlDataSource dataSource,
IDurableWorkRegistry workRegistry,
Guid runtimeEpoch,
Guid expectedStoreId,
NpgsqlTransaction transaction,
DurableWorkRequest request,
CancellationToken cancellationToken)
{
var options = new PostgreSqlDurableWorkOptions(
runtimeEpoch,
expectedStoreId,
PostgreSqlDurableWakeNotificationMode.Disabled);
var writer = new PostgreSqlDurableWorkTransactionWriter(dataSource, workRegistry, options);
var accepted = await writer.EnqueueAsync(transaction, request, cancellationToken);
if (!accepted.IsSuccess)
{
await transaction.RollbackAsync(cancellationToken);
return accepted;
}
await transaction.CommitAsync(cancellationToken);
return accepted;
}
// docs:snippet durable-postgresql-options-reuse:start
internal static PostgreSqlDurableWorkOptions CreateSharedOptions(
Guid runtimeEpoch,
Guid expectedStoreId)
{
// PostgreSqlDurableWorkOptions is reused directly across Work and Flow operations to guarantee
// consistent ExpectedStoreId, active RuntimeEpoch, notification modes, and schema compatibility validation.
return new PostgreSqlDurableWorkOptions(
runtimeEpoch,
expectedStoreId,
PostgreSqlDurableWakeNotificationMode.Disabled);
}
// docs:snippet durable-postgresql-options-reuse:end
}
The proof helper owns transaction completion: it rolls back when acceptance returns a domain problem and commits only
after successful Work acceptance. The writer itself uses the exact active NpgsqlTransaction; it never opens a second
connection, commits, rolls back, replaces, or disposes the caller transaction. Caller rollback removes both the domain
mutation and Work acceptance. Use
PostgreSqlDurableWorkClient with the same data source, registry, and options only when the package may own a short
acceptance transaction.
Work-first Schedule pass
PostgreSqlDurableScheduleClient persists the existing IDurableScheduleClient contract. The Gate A provider admits
At, After, and Every schedules whose target is registered Work; QueueOne and RunOnce are their defaults. It
captures one PostgreSQL transaction_timestamp() for each generation. After and unanchored Every derive their
first nominal time from that stored value, never from caller time or the later Work accepted_at value.
PostgreSqlDurableScheduleProcessor is intentionally passive. Construct it with a dispatcher data source and a
separate runtime-role data source, then invoke one bounded pass:
var processor = new PostgreSqlDurableScheduleProcessor(
dispatcherDataSource,
runtimeDataSource,
workRegistry,
workOptions,
new PostgreSqlDurableScheduleOptions("appsurface_durable_runtime"));
var pass = await processor.ProcessDueAsync(
new PostgreSqlDurableScheduleProcessRequest("orders-schedule-pass", maximumSchedules: 8),
cancellationToken);
The dispatcher can only lease the narrow Schedule queue through a security-definer claim function. It has no raw
schedule_dispatch table read, so due time and cadence remain inside the function; its result contains only scope,
Schedule ID, and revision. Before the processor sets scoped RLS state or bridges an occurrence, it verifies
current_user equals PostgreSqlDurableScheduleOptions.RuntimeRole. The Work bridge uses the existing caller-owned
PostgreSqlDurableWorkTransactionWriter, so the occurrence link and one Work acceptance commit or roll back together.
An empty pass returns zero counts. Cancellation stops before the next lease and cannot undo a previously committed fact.
Do not call the processor in a request loop. The worker host above is the standard continuous activation path; manual
processor calls remain useful only for focused tests or an explicitly designed external activator.
The Schedule processor also compares persisted runtime-epoch and scope-generation fences before evaluating a due row.
A mismatch suspends the Schedule before it can move a cursor or accept Work. Use ReleaseAfterRecovery only for an old
runtime epoch; a scope-generation mismatch must be repaired with a public update or delete/recreate. The dispatcher
claim function rejects blank/control-character owners and null or non-positive durations before leasing, preventing malformed
or overlong calls from stranding a row. Schedule discovery leases are capped at ten minutes.
For the admitted default QueueOne policy, one nonterminal target occupies the Schedule-wide slot. Later nominal
instants coalesce into one pending occurrence. When that Work reaches a terminal state, the Work transaction requeues
the Schedule dispatch row; the next manual pass materializes the pending occurrence immediately rather than waiting
for another interval. Retries and suspended Work intentionally retain the slot. See the normative
QueueOne occurrence rules for transaction ownership and
generation behavior.
ListAsync returns payload-free Schedule inventory ordered by Schedule ID. When the requested page is not terminal, it
returns a provider-issued continuation token; send that token back unchanged with the same scope and filters to obtain
the next page. The token is an opaque cursor, not an authorization grant, and changing it can only change the caller's
position within the already RLS-scoped inventory.
CronosV1, Flow targets, and non-default overlap or misfire policies are intentionally rejected by this increment. Cron
needs pinned evaluator/time-zone evidence; Flow has no caller-owned start transaction seam; Skip, bounded concurrency,
and catch-up need occurrence-state semantics that arrive in a later gate. A Schedule that observes a database-clock
advance beyond its configured safety window suspends instead of moving its cursor. ReleaseAfterRecovery only releases
an old-epoch fence; it cannot clear a clock/evaluator suspension or rewrite a cursor. Repair with a public definition
update or delete/recreate after the underlying cause is corrected.
Endpoint/database matching is a configuration guard. Durable identity is ExpectedStoreId, which the writer reads
through the supplied transaction. Notifications default to disabled; when enabled, they are payload-free latency hints
and never replace authoritative discovery.
Failure and effect safety
Local preflight and expected domain outcomes leave an otherwise active transaction usable. PostgreSQL errors, timeout,
network loss, server cancellation, or an aborting SQLSTATE require caller rollback. Savepoints are unsupported.
The API method being called is the operation context: failures are not wrapped in a generic provider exception that
would hide the concrete Npgsql type. Missing or incompatible schema failures expose safe Status; when PostgreSQL
reveals missing schema during Work acceptance, InnerException preserves the original PostgresException, stack, and
SQLSTATE. Only the outer durable message and status are safe to log. Never log or serialize the inner exception's
server-controlled message, detail, hint, SQL text, object names, or other fields; project only its concrete type and
SQLSTATE.
External provider I/O happens only after an exact-fence permit commits and never while a database connection or
transaction is held. Idempotent and ProviderKeyed work can recover safely; ReconcileBeforeRetry and
ManualResolution suspend ambiguous outcomes until evidence authorizes a transition. The package never claims
exactly-once external effects and never converts unknown post-permit truth to failed terminal.
The source provider implements audited reconciliation, manual-resolution, safe-retry, and recovery-release transitions as internal conformance behavior. Recovery release atomically moves an exact ambiguous permit to the newly authorized runtime epoch with its Work. When the current attempt has no exact ambiguous permit, release safely makes the Work retryable and leaves historical permits unchanged. When an expected exact permit cannot move with the Work, the entire release rolls back so later proof remains possible. The runtime pump, health, drain, and host composition are now public through the Provider SPI and PostgreSQL registration extensions. Applications must still keep authorization around all operator/control APIs and must not depend on internal PostgreSQL claim/store types.
Read the normative Work protocol v1, Flow protocol v1, Durable Flow trace context v1, the
ASDURxxx diagnostics catalog, the
slice 3 reconstruction ledger, and the slice 4 reconstruction ledger for exact behavior, safe responses, and lineage.
Verification
From the repository root, ./Durable/verify-postgresql.sh --quick runs focused Work proof, ./Durable/verify-postgresql.sh --quick --flow runs focused Flow proof, and ./Durable/verify-postgresql.sh --quick --schedule runs the real PostgreSQL Work-first Schedule proof. --ci runs the complete strict real-PostgreSQL suite; --ci --flow performs its compatibility preflight before that suite, while --ci --schedule also runs the complete suite without a Schedule-specific filter.
Release Guidance
This package has an explicitly experimental or publication-held contract. Do not treat it as a normal prerelease install; use the package chooser and release hub for the current publication decision, proof requirements, and migration guidance.