Skip to content

Operations runbook

Finite validation commands only; task dagster:dev starts a long-running UI and must be stopped with Ctrl-C.

Rebuild local warehouse from sources

task db:reset
task full-refresh

task full-refresh validates the configured Polaris catalog and AWS S3 writer, launches every source marked parallel_refresh=True concurrently as an independent Dagster job, verifies each authoritative Iceberg table and explicit _dlt_load_status, then runs project-wide native SQLMesh and every Soda contract only if the preceding phase succeeded. SOURCE_START/SOURCE_END lines and Dagster run IDs attribute interleaved logs; overlap is calculated from timestamps around each source's dg launch subprocess, proving worker-process overlap while including subprocess startup time. Raw data lives in S3-backed Iceberg tables registered by Polaris; data/databox.duckdb contains the local SQLMesh schemas such as environmental_observations and analytics.

Before refreshing, configure the Polaris client, AWS region, S3 bucket, and AWS runtime credentials documented in .env.example. Leave the session token empty for the accepted local long-lived key; provide its paired token for temporary OIDC/STS credentials. databox_lake must already be provisioned with s3://<bucket>/warehouse as its base location and the bucket-scoped IAM role, then start compose.iceberg.yml.

PostgreSQL first reports only basic database liveness. Polaris bootstrap then initializes the schema and realm. The one-shot catalog-backup-readiness service validates the primary runtime AWS credential against the separate pgBackRest repository and stanza, a WAL archive round trip, and an existing or newly created post-bootstrap full backup. The Polaris API remains stopped when any check fails; inspect the backup-readiness service output rather than bypassing protection.

Static pinned AVONET is deliberately excluded from routine refresh. Run its independent avonet_ingest Dagster job explicitly when a validated raw_avonet.species_traits replacement is required; it has no recurring schedule. The job preserves the pinned file/hash/schema checks and publishes the complete validated snapshot directly as a dlt-managed Polaris Iceberg table.

Smoke verification

task verify
cd transforms/main && ../../.venv/bin/sqlmesh test

task verify uses DATABOX_SMOKE=1 with the same concurrent Polaris Iceberg source path, then runs the same project-wide SQLMesh and Soda phases.

Protected live integration

The GitHub workflow .github/workflows/polaris-iceberg-integration.yaml is a manual, protected diagnostic gate rather than a durable refresh. Each of the six routine sources gets an independent job, disposable Polaris/Postgres state, and integration/<run>/<attempt>/<source>/warehouse S3 prefix. It uses GitHub OIDC; do not add static AWS credentials or automatic PR/push/schedule triggers.

Dispatch it from the GitHub Actions UI, approve the polaris-iceberg-integration environment, and inspect every matrix result. The workflow skips SQLMesh and does not delete integration objects. The first complete passing matrix was recorded on 2026-09-03; inspect the protected workflow's current GitHub Actions run for exact claims and limits.

Plan recovery infrastructure

Recovery infrastructure is declared in infra/recovery/ with OpenTofu >=1.8,<2. It creates one same-account, same-region catalog-backup bucket for pgBackRest. The accepted same-account and us-west-1 design does not protect against account-wide or regional failure. Iceberg warehouse objects are not copied: use Iceberg snapshots for logical rollback while objects remain, and rebuild complete warehouse loss from canonical sources.

Copy infra/recovery/terraform.tfvars.example to an ignored .tfvars file and replace every placeholder. Configure aws_profile in an AWS shared config file with renewable credentials; do not put credentials in OpenTofu variables or state. OpenTofu declares the console-only databox-recovery-operator user without an access key, login profile, password, or MFA seed. Its inline policy permits only assuming the bucket-scoped catalog-backup role and the two OAuth actions AWS requires for aws login --remote, scoped to the account's us-west-1 remote public client. It cannot use same-device login or other sign-in clients. After its separately reviewed apply, root must manually enable console access and MFA for that user; passwords, recovery codes, and MFA secrets must never enter OpenTofu configuration or state. The human then uses aws login --remote --profile databox-recovery-operator --region us-west-1 for the operator source profile and an AWS CLI role profile configured with role_arn, source_profile, and mfa_serial. The human enters MFA when calling AssumeRole; AWS returns ordinary short-lived role credentials for host injection, and pgBackRest never receives MFA data.

A separate declared inline policy, databox-warehouse-writer-inspection, grants that operator five IAM read/list actions against only databox-lake-user in var.aws_account_id: GetUser, ListUserPolicies, GetUserPolicy, ListAttachedUserPolicies, and ListGroupsForUser. It does not grant managed-policy document, group-policy or role-policy reads.

The user explicitly abandoned a prepared but unapplied stage-2 operator grant and chose the root-backed profile for the remaining bounded warehouse-recovery audit and infrastructure work, deferring deployer-role design until afterward. Keep raw IAM documents and identifiers private. Root use does not remove the fresh exact-plan review and explicit apply gate for warehouse changes.

The deployed inspection policy does not change the existing login policy, backup-role permissions, or warehouse bucket settings. It is not a complete effective-permission audit: boundaries, group policies, relevant role policies, cross-account references, resource policies, SCPs, session constraints, or other restrictions must remain explicit where unverified.

Manage this grant through aws_iam_user_policy.warehouse_writer_inspection in this root, not by editing the console's existing Terraform-named policy. If a same-named inspection policy was already created manually, stop before apply and reconcile ownership with separately authorized import or removal; do not silently overwrite it. Remove the inspection grant through a reviewed infrastructure change when its purpose is complete, preserving the operator's existing access.

var.aws_account_id remains the single account input for both the policy ARN and the provider's account guard. OpenTofu does not automatically read DATABOX_AWS_ACCOUNT_ID from .env; if that local value is maintained as well, confirm it matches the selected tfvars account before planning. A browser root login does not authenticate the CLI profile. For the current user-approved exception, use an explicitly authenticated root-backed CLI session for the remaining audit and separately approved plan/apply operations, then log it out. Never create root access keys for this workflow.

Review only—these commands do not apply infrastructure:

cd infra/recovery
tofu init -backend=false
tofu fmt -check
tofu validate
tofu plan -var-file=recovery.auto.tfvars -out=recovery.tfplan

plan is a saved live-refresh plan: it evaluates provider configuration, reads current managed-resource state, and requires the configured AWS profile. Review any reported drift separately; do not hide it with -refresh=false. Do not run tofu apply until the exact saved plan is reviewed and separately authorized. OpenTofu does not create, own, replace, destroy, relocate, or rewrite the primary Iceberg bucket; it declares only that existing bucket's policy, versioning setting, and lifecycle configuration.

OpenTofu state is intentionally local and operator-owned at infra/recovery/terraform.tfstate; always run init, plan, apply, and import from infra/recovery/. State and plan files are ignored by Git, and state must remain mode 0600 on the FileVault-protected host. A second backup copy is not required; the operator accepts manual state reconstruction after disk loss. Project cleanup commands must never delete these files. If state is lost, stop all changes and use reviewed tofu import commands for every existing resource, followed by a reviewed refresh-only plan; never recreate or apply over untracked live resources.

All recovery plans, journals, raw command exports, and receipts belong under the ignored /.recovery/ custody root with private file permissions. Never force-add raw recovery evidence; publish only reviewed, sanitized summaries under .ledger/ or durable documentation.

Warehouse object-version protection

warehouse_bucket is a required, private recovery.auto.tfvars input naming the existing warehouse bucket. Before plan/apply, verify it equals the local DATABOX_AWS_S3_BUCKET, differs from catalog_backup_bucket, is in the expected account/region, and has no newly appeared lifecycle or bucket policy. Terraform also checks that the active caller ARN is the exact account-root ARN before it reads the warehouse target; repeat that identity check immediately before apply because a saved plan does not pin credentials. Enabled/suspended or externally managed versioning, or any lifecycle/policy document, requires ownership reconciliation and possibly a separately approved import; never overwrite it blindly. Do not add a warehouse-bucket resource or output, and keep the private tfvars file mode 0600.

The warehouse policy is a temporary root-only retention-control boundary. It contains no Allow and exactly two Denies with Principal = "*" plus ArnNotEquals on aws:PrincipalArn against only the exact account-root ARN:

  • deny s3:DeleteObjectVersion on warehouse objects;
  • deny s3:PutBucketVersioning, s3:PutLifecycleConfiguration, s3:PutBucketPolicy, and s3:DeleteBucketPolicy on the bucket.

AWS documents that aws:PrincipalArn contains the IAM role ARN for role sessions and the exact root-user ARN for account-root requests. The policy therefore covers direct users, the known writer-assumable role, and unknown same- or cross-account role sessions while leaving actual account root as the temporary control identity. Do not substitute NotPrincipal, add a service exception, or broaden the root exception. A future non-root deployer must first be added to the exception through a separately reviewed root change.

State the guarantee narrowly: non-root principals cannot manually purge named versions or weaken/remove these three retention controls. Ordinary PutObject and DeleteObject remain available through existing identity policies; with versioning, an ordinary delete normally creates a delete marker. Non-root principals that can read a historical version and write an object can still promote that content as a new current version. Root and S3 Lifecycle remain intentional permanent-deletion paths. Bucket deletion, Object Lock, unrelated bucket security settings, current-object corruption, version storms, storage cost, root/account compromise, and losses detected after expiration are outside this guarantee.

Enable versioning bucket-wide, then configure exactly one bucket-wide lifecycle rule expiring noncurrent versions after 30 days. Do not add current-version expiration, transitions, delete-marker cleanup, newer-version count, multipart cleanup, or Iceberg maintenance. Lifecycle expiration is not blocked by the bucket-policy Deny. Thirty days starts when a version becomes noncurrent and is UTC-rounded/asynchronous, not immutable retention or a guaranteed 720 hours. Existing null versions gain no retroactive history; after their next overwrite or delete, the null version becomes noncurrent and enters lifecycle handling.

Immediately before a saved live-refresh plan, re-read policy, lifecycle, and versioning and stop if any expected-absent singleton appeared. A clean declaration expects three creates—warehouse policy, versioning, and lifecycle—with all catalog and IAM resources no-op and no output change. Keep the plan/log mode 0600, inspect resolved JSON privately, hash the exact binary and inputs, and obtain explicit approval. Do not use -refresh=false.

Before apply, a named maintenance owner must confirm PUT/DELETE traffic is quiet across all three prefixes. Recheck the exact target, root identity, saved-plan hash/fingerprints, and absent live singletons; apply only the approved binary. After versioning reads back Enabled, keep every write path paused for a fresh full 15 minutes, per AWS's first-enablement guidance, while verifying policy, lifecycle, public-access block, AES256 encryption, BucketOwnerEnforced ownership, and unrelated settings. On partial failure, preserve the pause and diagnostics; do not suspend versioning, delete controls, destroy resources, or retry blindly.

This protection preserves prospective object history only. It does not recover objects already lost, prove a denial, or prove coherent Iceberg recovery. Those claims require a separately approved disposable-table drill that aligns catalog, metadata, manifests, data/delete files, and query results.

Real Iceberg object-recovery drill

scripts/platform/iceberg_recovery_drill.py is one manual Stage-1 drill. It is never scheduled and never uses the active Polaris service. Each run derives a private Docker network, PostgreSQL volume, transient PostgreSQL/bootstrap/Polaris containers, generated catalog/namespace/table, and integration/recovery/<run-id>/stage1/warehouse/ S3 scope from one random token. Canonical catalogs, tables, volumes, services, and warehouse/ remain outside its accepted names.

The CLI has two commands but two independently approved mutation plans. prepare is mutation-free: it reads local configuration, inspects already installed Docker images/runtime without pulling or building, pins exact image IDs, launch/default-environment/user/working-directory configuration, and atomically writes a private Seed-A plan. It does not call AWS, S3, Polaris, or a Docker create/start/remove operation. Plan and initial-journal publication is atomic no-replace: an existing or concurrently published receipt is never overwritten.

task iceberg:recovery-drill:prepare

Privately inspect .recovery/iceberg/<run-id>/seed-a.plan.json, verify its exact SHA-256, and obtain separate explicit approval before seed execution. Then use the same generic executor with exactly that reviewed filename/hash:

task iceberg:recovery-drill:execute -- \
  --manifest .recovery/iceberg/REPLACE_RUN_ID/seed-a.plan.json \
  --sha256 REPLACE_APPROVED_SEED_SHA256

Seed execution resolves the approved profile once to one temporary credential set, verifies that exact set with STS against the normalized non-root caller digest, and reuses it for every direct S3 call and Polaris bootstrap. It never re-resolves the profile during the run. It also rechecks source/runtime/image pins, bucket owner/region, Enabled versioning, the single bucket-wide 30-day noncurrent rule, the exact two-Deny/root-exception policy, empty generated prefix, and absent resource collisions before mutation. It creates only the plan-named network/volume and transient secret-bearing containers. PostgreSQL, Polaris, and temporary AWS credentials are passed to bounded child processes through stdin, never command arrays, plans, Docker environment configuration, or stdout. Seed-A runs a versioning canary with the pinned credential set for put/ordinary delete and exact historical-version read/promotion. Recovery-A repeats that canary with its newly exported, STS-verified credential set after fresh point-A validation and immediately before publishing the damage marker. Restore-only re-entry never reruns the canary or continues damage.

The isolated catalog then receives one deterministic three-row Iceberg v2 table. The script requires every catalog-loaded FileIO to contain nonempty Polaris-vended access, secret, and session credentials, fences every FileIO operation before access, captures the metadata → manifest-list → manifest → data graph, and records exact VersionIds, ETags, sizes, and SHA-256 values. It rejects null versions, external locations, non-point-A entries, more than 64 objects, more than 32 MiB before body download, or insufficient version-count headroom. Secret-bearing containers are removed before the retained network/volume inspection state is fingerprinted and the atomic manifest.json Recovery-A plan is written; the labeled network, PostgreSQL volume, private plans, generated catalog state, and S3 history remain.

Seed success does not authorize damage. Privately inspect the resulting Recovery-A filename/hash and obtain a second explicit approval:

task iceberg:recovery-drill:execute -- \
  --manifest .recovery/iceberg/REPLACE_RUN_ID/manifest.json \
  --sha256 REPLACE_APPROVED_RECOVERY_SHA256

Recovery execution rechecks the Recovery-A/Seed-A relationship, settings, identities, images, exact retained network/volume fingerprint, catalog fingerprint, point-A pointer, every current source version, and all content before damage. It inventories all running/stopped exact-name containers, retained-network attachments, and retained-volume consumers. Only ID-bound PostgreSQL/Polaris sleeper remnants with the exact pinned launch, environment, labels, mount, privilege, and loopback port contract may be removed; bootstrap, changed, unknown, or volume-only consumers are refused. It starts only transient containers against the run-owned PostgreSQL volume after proving that volume unused. A per-key, mode-0600, atomically replaced and fsynced damage-started.json records delete and promotion intent plus observed VersionIds. Ordinary deletes run leaves first and metadata root last; break proof requires a healthy namespace, exact approved missing-object failure, current markers, and independently readable historical sources. Restoration promotes each exact source version, verifies the new current bytes/version while preserving the source, and then requires the exact UUID, pointer, snapshot, logical graph, schema, rows, count, and digest.

After the journal exists, rerunning the same Recovery-A command is restore-only, even if its six-hour first-damage approval window has expired. An expired plan with no exact bound journal contains any fully validated credential-bearing remnants and then stops before AWS preflight or damage. Resume never deletes; it accepts only journal-attributable markers/promoted versions, refuses unknown current state, and reports recovered-after-interruption instead of pass. Exact owned recovery remnants may be contained before restart; unknown resources are refused. Transient containers are removed on success or caught failure, while network, volume, catalog evidence, plans, object versions, markers, and restored current objects are preserved.

Configure one explicit profile name and canonical caller hash in ignored .env. The already-authenticated non-root profile must be in the same AWS account as DATABOX_RECOVERY_STORAGE_ROLE_ARN and is used for both ordinary-delete and exact-version recovery operations. The project-wide DATABOX_AWS_ROLE_ARN remains unchanged. The operator may be an IAM user or assumed role; the separately plan-bound dedicated Stage-1 role remains Polaris's vended-credential role. Also generate one private run-secret of at least 32 random characters; only run-scoped HMAC-derived PostgreSQL/Polaris credentials exist in memory and only an HMAC binding is saved. The script never logs in or prints caller identities/credentials.

DATABOX_RECOVERY_STORAGE_ROLE_ARN=
DATABOX_RECOVERY_PROFILE=
DATABOX_RECOVERY_IDENTITY_SHA256=
DATABOX_RECOVERY_RUN_SECRET=

Generate the caller hash privately from canonical get-caller-identity output. The projection normalizes an assumed-role session ARN to its durable IAM role ARN, so a normal session refresh cannot strand restore-only recovery:

profile=REPLACE_EXPLICIT_PROFILE
aws --profile "$profile" sts get-caller-identity --output json --no-cli-pager \
  | .venv/bin/python -c '
import hashlib, json, re, sys
identity = json.load(sys.stdin)
arn = identity["Arn"]
assumed = re.fullmatch(
    r"arn:(aws(?:-us-gov)?):sts::([0-9]{12}):assumed-role/(.+)/[^/]+", arn
)
if assumed:
    arn = f"arn:{assumed[1]}:iam::{assumed[2]}:role/{assumed[3]}"
payload = json.dumps(
    {"account": identity["Account"], "arn": arn},
    sort_keys=True,
    separators=(",", ":"),
) + "\n"
print(hashlib.sha256(payload.encode()).hexdigest())
'

This Stage-1 run proves reconstruction, not separate-principal privilege separation; that stronger proof is explicitly out of scope for the solo workflow. The script strips ambient AWS/profile/web/container/legacy-boto providers and HTTP proxies before PyIceberg is constructed, rejects AWS account root, fences all direct S3 calls with the expected bucket owner, and refuses IAM broadening or fallback. It never calls DeleteObjectVersion, removes a marker, alters bucket controls, guesses metadata, drops the catalog, or removes the retained network or volume. Cleanup is separate, non-purging, unimplemented, and requires its own reviewed exact plan and approval.

This proves coherent object reconstruction only for one constrained synthetic Iceberg v2 table with its isolated catalog pointer intact. It does not prove catalog PITR, negative policy enforcement, arbitrary-table recovery, bucket/account recovery, or recovery after lifecycle expiration.

Stage 2A: local POSIX catalog PITR

Run the synthetic, cloud-free proof with:

task catalog:recovery-stage2a

It creates only generated Docker resources, bootstraps the real Polaris relational schema, writes deterministic point-A state, takes a POSIX pgBackRest backup, commits later point-B state, and restores a separate PostgreSQL volume to a named target. Success requires point A to exist, point B to be absent, PostgreSQL to be promoted, and Polaris readiness against the recovered schema. Credential-bearing containers are removed; the generated network, source/restored volumes, POSIX repository volume, and ignored mode-0600 result are retained. It never contacts AWS or active services, reads canonical data, cuts over, or cleans up resources.

The deployed S3 backup repository, authenticated WAL catch-up, canonical catalog validation, and production RPO/RTO belong to separately authorized Stage 2B.

Stage 2C: joint isolated catalog and warehouse recovery

Stage 2C composes local POSIX catalog PITR with exact-version recovery in the real versioned warehouse sandbox. It is limited to exactly two generated tables under integration/recovery/<run-id>/stage1/warehouse/, one point-A backup/target, one point-B append per table, at most 64 keys and 32 MiB, and one damage cycle. It never uses active/canonical services, the deployed catalog-backup repository or credentials, root, IAM changes, cutover, version purging, or automatic cleanup.

Prepare a mutation-free private campaign plan, then execute only that exact plan:

task catalog:recovery-stage2c -- prepare
task catalog:recovery-stage2c -- execute \
  --plan .recovery/catalog-warehouse-stage2c/<run-id>/campaign.plan.json \
  --sha256 <exact-private-plan-sha256>

The success path has a 20-minute objective. Every child operation is individually bounded; after the durable damage journal exists, deadline expiry enters mandatory restoration and containment rather than abandoning damaged objects. Re-entry is restoration-only. Generated Docker resources are accepted or removed only after exact campaign-bound image, label, mount, network, port, attachment, and volume- consumer checks. Final success requires both restored catalog pointers, UUIDs, snapshots, logical graphs, schemas, and deterministic rows to match point A while point-B-only objects remain unreferenced. All generated resources and private evidence are retained.

The first authorized live campaign stopped before point A because its ownership validator did not distinguish Docker's empty configured ephemeral host port from the numeric runtime loopback assignment. A later authorization allowed three fresh attempts with at most one damage-bearing cycle. The first two stopped contained before damage. The third completed deletes and break proof, then stopped during catalog restoration because Docker represents --network none as a built- in runtime network entry rather than an empty network map. Exact object restoration completed automatically; after correcting that fail-closed validator, mandatory catalog restoration and containment also completed and were independently checked.

That campaign is not a Stage-2C success: final joint Polaris pointer, graph, schema, and row validation did not run after the terminal failure. All generated containers are absent, while prefixes, networks, volumes, and private evidence are retained. Every plan is consumed, the three-attempt envelope and one damage cycle are exhausted, and no fresh live execution is authorized. Do not replay any plan or infer success from the post-failure restoration receipt.

Two runtime details are now explicit in the maintained implementation. After PostgreSQL is restarted into archival mode for the point-A backup, source Polaris is recreated and the cached gateway is discarded so its JDBC pool and ephemeral loopback endpoint cannot remain stale. A restore helper is accepted only on Docker's built-in none network with no aliases, DNS names, addresses, gateways, or MAC address; any connected shape is rejected.

A separately authorized validation-only continuation can complete the missing final read against that exact restored retained state without replaying recovery:

task catalog:recovery-stage2c -- prepare-validation \
  --plan .recovery/catalog-warehouse-stage2c/<run-id>/campaign.plan.json \
  --sha256 <exact-original-campaign-plan-sha256>
task catalog:recovery-stage2c -- validate \
  --plan .recovery/catalog-warehouse-stage2c/<run-id>/validation.plan.json \
  --sha256 <exact-validation-plan-sha256>

The validation plan hash-binds the campaign, recovery plan, damage journal, terminal failure, restoration correction, detached Docker resource projections, and exact bounded-prefix version timelines. Validation starts only the retained restored PostgreSQL volume with archival disabled and the restored Polaris service; it never runs backup/restore or changes warehouse objects. It is one-shot and has a ten-minute objective plus per-command bounds. A separate watchdog removes credential-bearing containers on objective expiry; this is not a hard whole-process or RTO claim. Interrupted re-entry performs Docker-only containment without current AWS credentials, source/image freshness, or plan-expiry gates and never revalidates. Success proves eventual point-A catalog/warehouse coherence, point-B exclusion, and unchanged object-version timelines. It does not rewrite the original terminal evidence or claim uninterrupted recovery or the 20-minute RTO.

The retained validation invocation described here is now consumed and must not be replayed. Its substantive promoted-version and two-table Polaris checks passed, but the command stopped failed-contained at a later inventory predicate that compared new promoted-copy ETags with historical-source ETags. Read-only diagnosis found all 19 expected keys, unchanged exact timelines, exact current bytes, and intact source versions; only the four promoted ETags differed. Because ETag is version metadata, not the cross-version content identity, the maintained inventory check now verifies exact SHA-256/size plus recorded source/promoted VersionIds instead. The append-only adjudication supports eventual coherent recovery only; the validation command, original automation, and RTO remain unproven.

Recovery-drill cleanup

Cleanup is a separately authorized, one-shot terminal operation. It retains private /.recovery/ evidence, ordinary-deletes only live objects under exact generated drill prefixes, and removes only exact ownership-validated drill Docker remnants. Ordinary deletion creates new delete markers; it never deletes object versions or removes markers. Canonical resources, active Compose services, bucket controls, IAM, and the deployed backup repository are outside its mutation scope.

task recovery:cleanup -- prepare
task recovery:cleanup -- execute \
  --plan .recovery/cleanup/<run-id>/plan.json \
  --sha256 <exact-plan-sha256>

Preparation is read-only apart from its private mode-0600 plan. Execution first rederives the complete authorized inventory, requires unchanged source evidence, exact S3 timelines, exact Docker identities/configuration/attachments, healthy active services, and the approved bucket protections. It durably writes intent before mutation, immediately rechecks each target, uses no force removal, and publishes a private terminal result. Public output contains counts only. A terminal plan cannot be replayed. Cleanup does not authorize another drill, RTO rehearsal, negative IAM test, cutover, or deletion of private evidence.

Catalog backup and recovery preparation

The PostgreSQL image includes pgBackRest and archives WAL with archive_timeout=300s. pgBackRest reuses the primary DATABOX_AWS_* runtime credential: the local databox-lake-user credential has no session token, while CI's temporary OIDC credential includes one. The catalog-backup bucket policy grants these runtime principals only the bucket/object operations pgBackRest requires and denies non-root version deletion and protection changes. Production uses repository path /polaris; protected CI sets a run/source-isolated path. Configure the OpenTofu catalog-backup output and PGBACKREST_REPO1_CIPHER_PASS; never commit or log runtime secrets. The MFA-protected catalog-backup role remains for human recovery and is not injected into routine containers. The PostgreSQL image does not install AWS CLI or mount host AWS profiles. The production repository path remains /polaris. Run task catalog:backup-check before the weekly catalog:backup-full or daily catalog:backup-diff, and inspect task catalog:backup-info after each run. All four manual commands execute pgBackRest as the container's postgres user. These commands are not scheduled automatically and do not prove the recovery objectives.

Prepare—do not execute—an isolated restore with a new Docker volume name:

uv run python scripts/platform/catalog_recovery.py \
  --target-volume databox_polaris_recovery_20260905 \
  --active-volume databox_polaris_postgres \
  --recover-to 2026-09-05T12:00:00Z \
  --prepare-only

Preparation rejects the active volume, malformed names or timestamps, missing backup settings, and any target volume that already exists. It creates no volume or container. The explicit --execute path is implemented but requires separate live-restore authorization; it creates the target, initializes only its ownership, and runs the pinned pgBackRest image as postgres with secret values inherited by environment-variable name. It mounts no active volume or socket, opens no port, never removes the target on failure, and stops before PostgreSQL or Polaris startup, validation, or cutover.

For a separately authorized complete timed drill, run the existing recovery entrypoint from an interactive operator terminal:

task catalog:recovery-drill -- \
  --catalog databox_lake \
  --source-revision "$(git rev-parse HEAD)"

The command normally performs one AWS remote login and MFA-protected role export on the operator TTY. When the operator has just completed that login and primed the backup-role session in the same AWS CLI configuration, pass --reuse-authenticated-session to skip only the redundant login prompt; the command still exports the short-lived role session directly into memory and refuses credentials with less than 15 minutes remaining. That minimum prevents near-expiry cache reuse; it does not guarantee the 60-minute RTO objective. Before creating a marker, it enumerates every locally retained .ready WAL file, validates the bounded list and corresponding regular files, and synchronously uploads every segment oldest-first with the same session. It never renames or deletes WAL or archive-status files. After switching and pushing the marker segment, it derives a bounded same-timeline sequence from the segment before the oldest pending file through the marker and synchronously retrieves each segment from the repository into one exact /dev/shm verification path. pgBackRest validates each retrieved segment and the command removes only that transient path; any missing segment or cleanup failure stops before restore. This catch-up does not retroactively protect changes that existed only on the local machine before the command ran.

Temporary credentials are never written to .env, a handoff file, or preserved container configuration. Recovery PostgreSQL starts inside a secret-free sleeper container and receives backup credentials only in its first detached child process while PITR fetches WAL. After promotion and marker validation, the container is stopped, restarted, and PostgreSQL is launched again without backup credentials; any earlier failure stops the credential-bearing process. Polaris follows the same secret-free sleeper pattern, receives credentials only in a detached child process, and is always stopped after validation or failure while its container remains preserved. The command uses a microsecond-precise marker bracket, synchronous marker and cleanup WAL pushes, a new ownership-labeled volume, unexposed archive-disabled PostgreSQL, no-bootstrap Polaris, and the registry-derived validator. Its report records pending count, oldest/newest segment, oldest pending mtime and age at command start, ordered uploads, observed repository maximum, continuity anchor/verified-through marker, and the marker target-inclusion gap. The observed maximum is informational and is never treated as continuity proof; the report does not label the marker gap as continuous RPO. End-to-end RTO starts before authentication, and a result over 3,600 seconds exits nonzero. It never restarts the active stack, cuts over, or deletes recovery resources.

After separately authorized restored PostgreSQL and Polaris startup, validate only the explicitly named no-port recovery container:

uv run python scripts/platform/catalog_recovery_validate.py \
  --polaris-container databox-polaris-recovery-validation-20260908-214022 \
  --catalog databox_lake \
  --recovery-target 2026-09-08T21:40:22Z \
  --source-revision e27990e

The validator resolves --source-revision to a commit and fails unless that commit's canonical registry bytes exactly match the imported working-tree registry. The example uses the reviewed corrected validation-contract revision; record the distinct historical code revision for the selected recovery point in the drill evidence. It derives every expected raw table and per-source _dlt_load_status from databox.config.sources.SOURCES. It rejects a container that lacks the recovery-validation label, is stopped, or publishes a host port. It enumerates the restored catalog, loads every expected table with Polaris-vended credentials, requires a current snapshot, plans its manifests, and performs a read-only limit-one data scan. Empty tables pass when that path completes with zero rows. Output is bounded and secret-free. Missing, malformed, unreadable, or undeclared tables inside canonical namespaces exit nonzero. Namespaces and tables outside the canonical registry remain prominent warnings and do not fail recovery by themselves; they are never silently allowlisted or deleted. The validator never contacts the active catalog, bootstraps, writes, refreshes sources, cuts over, or cleans up recovery artifacts. Run it only after separately authorizing the exact recovery target and container.

A bad table publication should use a validated Iceberg snapshot rollback while its objects remain. Complete primary-warehouse loss requires source rebuild and is not covered by the 60-minute catalog RTO. Last-resort table registration must use a validated metadata location, never lexicographic S3 listing. Live PITR execution and the timed catalog RPO/RTO drill remain separately authorized work.

SQLMesh dev loop

cd transforms/main
../../.venv/bin/sqlmesh plan dev --auto-apply --no-prompts
../../.venv/bin/sqlmesh test

Dev schemas use the __dev suffix, for example environmental_observations__dev.fact_bird_observation.

CDM row-count sanity checks

SELECT COUNT(*) FROM environmental_observations.fact_bird_observation;
SELECT COUNT(*) FROM environmental_observations.fact_weather_observation;
SELECT COUNT(*) FROM environmental_observations.fact_streamflow_observation;
SELECT COUNT(*) FROM environmental_observations.fact_earthquake_event;
SELECT COUNT(*) FROM analytics.platform_health;

Primary key duplicate checks should return zero:

SELECT COUNT(*) - COUNT(DISTINCT bird_observation_sk)
FROM environmental_observations.fact_bird_observation;

SELECT COUNT(*) - COUNT(DISTINCT weather_observation_sk)
FROM environmental_observations.fact_weather_observation;

SELECT COUNT(*) - COUNT(DISTINCT streamflow_observation_sk)
FROM environmental_observations.fact_streamflow_observation;

SELECT COUNT(*) - COUNT(DISTINCT earthquake_event_sk)
FROM environmental_observations.fact_earthquake_event;

Broken local file recovery

mv data/databox.duckdb data/databox.duckdb.broken
task full-refresh

If the rebuild fails, restore the backup and inspect .logs/ plus Dagster run history under .dagster/.

Rollback SQLMesh prod

SQLMesh plan history is the rollback mechanism:

cd transforms/main
../../.venv/bin/sqlmesh state list
../../.venv/bin/sqlmesh plan prod --restore-from <previous-plan-id> --auto-apply

UI launch

task dagster:dev

This command is intentionally long-running. Stop it with Ctrl-C after manual UI inspection.