Query icon
If icon
IonToParquet icon
Upload icon
Download icon
Query icon
Assert icon
SlackIncomingWebhook icon
Pause icon
Log icon
Webhook icon

Archive old Postgres rows to Parquet, verify the archive, then purge after approval

Archive old Postgres rows to Parquet in MinIO/S3, verify by read-back, and purge only after a typed human confirmation in Kestra.

Categories
DataInfrastructure

Deleting old rows from a hot Postgres table is usually a manual "export, then DELETE" with no proof that the export is readable. One bad dump means permanent data loss, and nobody can say afterwards what was removed.

This blueprint makes the proof and the approval part of the pipeline:

  • Exports every row older than retention_days to Parquet and uploads it to MinIO/S3.
  • Downloads the object back from the bucket and counts it with DuckDB, so it verifies what is actually stored, not what was sent.
  • Pauses for a human to type the table name before anything is deleted.
  • Re-checks that the source did not change while waiting, then purges and confirms the deleted count equals the archived count.

Rows are deleted only if archived rows, exported rows and the rows still in Postgres at purge time all agree.

How it works

  1. archive_request (io.kestra.plugin.core.trigger.Webhook) starts the flow; the body fields table, date_column and retention_days map onto the flow inputs.
  2. count_candidates (io.kestra.plugin.jdbc.postgresql.Query) counts rows older than the cutoff, and has_rows (io.kestra.plugin.core.flow.If) ends the run early when there is nothing to archive.
  3. export_rows (io.kestra.plugin.jdbc.postgresql.Query) streams the rows to internal storage, to_parquet (io.kestra.plugin.serdes.parquet.IonToParquet) converts them, and upload_archive (io.kestra.plugin.minio.Upload) writes the object to the bucket.
  4. download_back (io.kestra.plugin.minio.Download) reads the object back and count_archive (io.kestra.plugin.jdbc.duckdb.Query) counts its rows; verify_archive (io.kestra.plugin.core.execution.Assert) fails the run if the counts differ.
  5. ask_slack (io.kestra.plugin.core.flow.If) optionally sends the approval request through notify_approval (io.kestra.plugin.slack.notifications.SlackIncomingWebhook).
  6. approve_purge (io.kestra.plugin.core.flow.Pause) waits for a human to type the table name, up to two days. If nobody answers, the execution is cancelled and nothing is deleted.
  7. confirm_name and still_matches (io.kestra.plugin.core.execution.Assert) check the typed name and that a fresh recount still equals the archived count.
  8. purge (io.kestra.plugin.jdbc.postgresql.Query) deletes the rows and verify_purge checks the deleted count against the archived count.

What you get

  • Verified archives: the Parquet object is read back from the bucket before any row is removed.
  • A human gate on the destructive step: a wrong table name fails the run and keeps every row.
  • Safe on failure: any failed check stops the flow before the DELETE, and the archive stays in the bucket.
  • Reusable: the table, date column, retention and bucket are inputs, so one flow serves several tables.

Who it's for

  • Platform and data engineers who pay for storage on a hot OLTP table and need to trim it.
  • DBAs who want retention enforced with evidence instead of an ad hoc script.
  • Teams that need an auditable record of what was archived and who approved the purge.

Why orchestrate this with Kestra

A shell script can export and delete, but it cannot pause for approval, verify the stored object independently, or leave a per-run audit trail. Kestra gives the flow a webhook entry point, a Pause that survives restarts, assertions that stop the run on a mismatch, and a history of every execution with the exact counts at each step.

Prerequisites

  • A PostgreSQL database with a table that has a timestamp or date column.
  • A MinIO or S3-compatible bucket (default name cold-archive) that already exists.
  • Optional: a Slack incoming webhook for the approval message.

Secrets

  • POSTGRES_URL: JDBC URL of the source database, for example jdbc:postgresql://host:5432/db.
  • POSTGRES_USERNAME: database user with read and delete rights on the table.
  • POSTGRES_PASSWORD: password for that user.
  • MINIO_ENDPOINT: endpoint URL of the MinIO or S3-compatible store.
  • MINIO_ACCESS_KEY_ID: access key for the bucket.
  • MINIO_SECRET_KEY_ID: secret key for the bucket.
  • ARCHIVE_WEBHOOK_KEY: key segment that secures the webhook URL.
  • SLACK_WEBHOOK_URL: Slack incoming webhook, only needed when notify_slack is true.

Inputs

  • table (STRING, default events): lowercase unquoted table name, regex-validated because it is interpolated into SQL.
  • date_column (STRING, default created_at): timestamp or date column used for the cutoff, same validation.
  • retention_days (INT, default 365, minimum 30): rows older than this are archived, then purged.
  • bucket (STRING, default cold-archive): destination bucket.
  • notify_slack (BOOL, default false): send the approval request to Slack.

Quick start

  1. Add the secrets above to your Kestra namespace and create the bucket.
  2. Upload the flow and execute it from the UI, or call the webhook:
    curl -X POST "https://kestra.example.com/api/v1/main/executions/webhook/company.team/postgres-verified-cold-archive/<ARCHIVE_WEBHOOK_KEY>" \
      -H "Content-Type: application/json" \
      -d '{"table":"events","date_column":"created_at","retention_days":365}'
    
  3. Wait for the execution to pause at approve_purge, then resume it and type the table name.
  4. Check log_done for Archived and purged N rows ... and find the object at s3://<bucket>/archive/<table>/cutoff=<date>/<execution id>.parquet.

A paused run holds the flow's concurrency slot until it is resumed or times out.

How to extend

  • Several tables: wrap the logic in a Subflow and call it once per table.
  • Cold storage lifecycle: add a bucket lifecycle rule on archive/ to expire old archives.
  • Stricter verification: compare a column checksum between the source and the archive before approving.
  • Different object store: swap minio for aws.s3 or gcp.gcs uploads and downloads.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.